// lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx
"use client"
import * as React from "react"
import { useSearchParams } from "next/navigation"
import { type DataTableAdvancedFilterField, type DataTableFilterField } 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 { TechSalesVendorQuotations, TECH_SALES_QUOTATION_STATUSES, TECH_SALES_QUOTATION_STATUS_CONFIG } from "@/db/schema"
import { useRouter } from "next/navigation"
import { getColumns } from "./vendor-quotations-table-columns"
import { TechSalesRfqAttachmentsSheet, ExistingTechSalesAttachment } from "../../table/tech-sales-rfq-attachments-sheet"
import { getTechSalesRfqAttachments, getVendorQuotations } from "@/lib/techsales-rfq/service"
import { toast } from "sonner"
import { Skeleton } from "@/components/ui/skeleton"
interface QuotationWithRfqCode extends TechSalesVendorQuotations {
rfqCode?: string | null;
materialCode?: string | null;
dueDate?: Date;
rfqStatus?: string;
itemName?: string | null;
projNm?: string | null;
quotationCode?: string | null;
quotationVersion?: number | null;
rejectionReason?: string | null;
acceptedAt?: Date | null;
attachmentCount?: number;
}
interface VendorQuotationsTableProps {
vendorId: string;
}
// 로딩 스켈레톤 컴포넌트
function TableLoadingSkeleton() {
return (
{/* 툴바 스켈레톤 */}
{/* 테이블 헤더 스켈레톤 */}
{/* 테이블 행 스켈레톤 */}
{Array.from({ length: 5 }).map((_, index) => (
))}
{/* 페이지네이션 스켈레톤 */}
)
}
// 중앙 로딩 인디케이터 컴포넌트
function CenterLoadingIndicator() {
return (
데이터를 불러오는 중...
잠시만 기다려주세요.
)
}
export function VendorQuotationsTable({ vendorId }: VendorQuotationsTableProps) {
const searchParams = useSearchParams()
const router = useRouter()
// 첨부파일 시트 상태
const [attachmentsOpen, setAttachmentsOpen] = React.useState(false)
const [selectedRfqForAttachments, setSelectedRfqForAttachments] = React.useState<{ id: number; rfqCode: string | null; status: string } | null>(null)
const [attachmentsDefault, setAttachmentsDefault] = React.useState([])
// 데이터 로딩 상태
const [data, setData] = React.useState([])
const [pageCount, setPageCount] = React.useState(0)
const [total, setTotal] = React.useState(0)
const [isLoading, setIsLoading] = React.useState(true)
const [isInitialLoad, setIsInitialLoad] = React.useState(true) // 최초 로딩 구분
// URL 파라미터에서 설정 읽기
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') || '',
to: searchParams?.get('to') || '',
}), [searchParams])
// 데이터 로드 함수
const loadData = React.useCallback(async () => {
try {
setIsLoading(true)
console.log('🔍 [VendorQuotationsTable] 데이터 로드 요청:', {
vendorId,
settings: initialSettings
})
const result = await getVendorQuotations({
page: initialSettings.page,
perPage: initialSettings.perPage,
sort: initialSettings.sort,
filters: initialSettings.filters,
joinOperator: initialSettings.joinOperator,
basicFilters: initialSettings.basicFilters,
basicJoinOperator: initialSettings.basicJoinOperator,
search: initialSettings.search,
from: initialSettings.from,
to: initialSettings.to,
}, vendorId)
console.log('🔍 [VendorQuotationsTable] 데이터 로드 결과:', {
dataLength: result.data.length,
pageCount: result.pageCount,
total: result.total
})
setData(result.data as QuotationWithRfqCode[])
setPageCount(result.pageCount)
setTotal(result.total)
} catch (error) {
console.error('데이터 로드 오류:', error)
toast.error('데이터를 불러오는 중 오류가 발생했습니다.')
} finally {
setIsLoading(false)
setIsInitialLoad(false)
}
}, [vendorId, initialSettings])
// URL 파라미터 변경 감지 및 데이터 재로드 (초기 로드 포함)
React.useEffect(() => {
loadData()
}, [
searchParams?.get('page'),
searchParams?.get('perPage'),
searchParams?.get('sort'),
searchParams?.get('filters'),
searchParams?.get('joinOperator'),
searchParams?.get('basicFilters'),
searchParams?.get('basicJoinOperator'),
searchParams?.get('search'),
searchParams?.get('from'),
searchParams?.get('to'),
// vendorId 변경도 감지
vendorId
])
// 데이터 안정성을 위한 메모이제이션
const stableData = React.useMemo(() => {
return data;
}, [data.length, data.map(item => `${item.id}-${item.status}-${item.updatedAt}`).join(',')]);
// 첨부파일 시트 열기 함수
const openAttachmentsSheet = React.useCallback(async (rfqId: number) => {
try {
// RFQ 정보 조회 (data에서 rfqId에 해당하는 데이터 찾기)
const quotationWithRfq = data.find(item => item.rfqId === rfqId)
if (!quotationWithRfq) {
toast.error("RFQ 정보를 찾을 수 없습니다.")
return
}
// 실제 첨부파일 목록 조회 API 호출
const result = await getTechSalesRfqAttachments(rfqId)
if (result.error) {
toast.error(result.error)
return
}
// API 응답을 ExistingTechSalesAttachment 형식으로 변환
const attachments: ExistingTechSalesAttachment[] = result.data.map(att => ({
id: att.id,
techSalesRfqId: att.techSalesRfqId || rfqId,
fileName: att.fileName,
originalFileName: att.originalFileName,
filePath: att.filePath,
fileSize: att.fileSize || undefined,
fileType: att.fileType || undefined,
attachmentType: att.attachmentType as "RFQ_COMMON" | "VENDOR_SPECIFIC",
description: att.description || undefined,
createdBy: att.createdBy,
createdAt: att.createdAt,
}))
setAttachmentsDefault(attachments)
setSelectedRfqForAttachments({
id: rfqId,
rfqCode: quotationWithRfq.rfqCode || null,
status: quotationWithRfq.rfqStatus || "Unknown"
})
setAttachmentsOpen(true)
} catch (error) {
console.error("첨부파일 조회 오류:", error)
toast.error("첨부파일 조회 중 오류가 발생했습니다.")
}
}, [data])
// 테이블 컬럼 정의
const columns = React.useMemo(() => getColumns({
router,
openAttachmentsSheet,
}), [router, openAttachmentsSheet])
// 필터 필드
const filterFields = React.useMemo[]>(() => [
{
id: "status",
label: "상태",
options: Object.entries(TECH_SALES_QUOTATION_STATUSES).map(([, statusValue]) => ({
label: TECH_SALES_QUOTATION_STATUS_CONFIG[statusValue].label,
value: statusValue,
}))
},
{
id: "rfqCode",
label: "RFQ 번호",
placeholder: "RFQ 번호 검색...",
},
{
id: "materialCode",
label: "자재 코드",
placeholder: "자재 코드 검색...",
}
], [])
// 고급 필터 필드
const advancedFilterFields = React.useMemo[]>(() => [
{
id: "rfqCode",
label: "RFQ 번호",
type: "text",
},
{
id: "materialCode",
label: "자재 코드",
type: "text",
},
{
id: "status",
label: "상태",
type: "multi-select",
options: Object.entries(TECH_SALES_QUOTATION_STATUSES).map(([, statusValue]) => ({
label: TECH_SALES_QUOTATION_STATUS_CONFIG[statusValue].label,
value: statusValue,
})),
},
{
id: "validUntil",
label: "유효기간",
type: "date",
},
{
id: "submittedAt",
label: "제출일",
type: "date",
},
], [])
// useDataTable 훅 사용
const { table } = useDataTable({
data: stableData,
columns,
pageCount,
rowCount: total,
filterFields,
enablePinning: true,
enableAdvancedFilter: true,
enableColumnResizing: true,
columnResizeMode: 'onChange',
initialState: {
sorting: initialSettings.sort,
columnPinning: { right: ["actions"] },
},
getRowId: (originalRow) => String(originalRow.id),
shallow: false,
clearOnDefault: true,
defaultColumn: {
minSize: 50,
maxSize: 500,
},
})
// 최초 로딩 시 전체 스켈레톤 표시
if (isInitialLoad && isLoading) {
return (
)
}
return (
{/* 로딩 오버레이 (재로딩 시) */}
{/* {!isInitialLoad && isLoading && (
)} */}
{!isInitialLoad && isLoading && (
)}
{/* 첨부파일 관리 시트 (읽기 전용) */}
{}} // 읽기 전용이므로 빈 함수
readOnly={true} // 벤더 쪽에서는 항상 읽기 전용
/>
);
}