summaryrefslogtreecommitdiff
path: root/lib/vendor-pool/table/vendor-pool-table.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendor-pool/table/vendor-pool-table.tsx')
-rw-r--r--lib/vendor-pool/table/vendor-pool-table.tsx806
1 files changed, 806 insertions, 0 deletions
diff --git a/lib/vendor-pool/table/vendor-pool-table.tsx b/lib/vendor-pool/table/vendor-pool-table.tsx
new file mode 100644
index 00000000..caf52865
--- /dev/null
+++ b/lib/vendor-pool/table/vendor-pool-table.tsx
@@ -0,0 +1,806 @@
+"use client"
+
+import * as React from "react"
+import type {
+ DataTableAdvancedFilterField,
+ DataTableFilterField,
+} from "@/types/table"
+import { useSession } from "next-auth/react"
+
+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 { Button } from "@/components/ui/button"
+import { toast } from "sonner"
+import { BulkImportDialog } from "./bulk-import-dialog"
+
+import { columns, type VendorPoolItem } from "./columns"
+import { createVendorPool, updateVendorPool, deleteVendorPool } from "../service"
+import { getVendorByTaxId } from "@/lib/vendors/service"
+import { getMaterialGroupDetail } from "@/lib/material-groups/services"
+import type { VendorPool } from "../types"
+import { cn } from "@/lib/utils"
+
+// 테이블 메타 타입 확장
+declare module "@tanstack/react-table" {
+ interface TableMeta<TData> {
+ onCellUpdate?: (id: string, field: keyof TData, newValue: any) => Promise<void>
+ onCellCancel?: (id: string, field: keyof TData) => void
+ onAction?: (action: string, data?: any) => void
+ onSaveEmptyRow?: (tempId: string) => Promise<void>
+ onCancelEmptyRow?: (tempId: string) => void
+ isEmptyRow?: (id: string) => boolean
+ getPendingChanges?: () => Record<string, Partial<VendorPoolItem>>
+ onTaxIdChange?: (id: string, taxId: string) => Promise<void>
+ onMaterialGroupCodeChange?: (id: string, materialGroupCode: string) => Promise<void>
+ }
+}
+
+interface VendorPoolTableProps {
+ data: VendorPoolItem[]
+ pageCount?: number
+ onRefresh?: () => void // 데이터 새로고침 콜백
+}
+
+export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableProps) {
+ const { data: session } = useSession()
+
+ // 수정사항 추적 (일괄 저장용)
+ const [pendingChanges, setPendingChanges] = React.useState<Record<string, Partial<VendorPoolItem>>>({})
+ const [isSaving, setIsSaving] = React.useState(false)
+
+ // 빈 행 관리 (신규 등록용)
+ const [emptyRows, setEmptyRows] = React.useState<Record<string, VendorPoolItem>>({})
+ const [isCreating, setIsCreating] = React.useState(false)
+
+ // 일괄입력 다이얼로그 상태
+ const [bulkImportDialogOpen, setBulkImportDialogOpen] = React.useState(false)
+
+
+
+ // 인라인 편집 핸들러 (일괄 저장용)
+ const handleCellUpdate = React.useCallback(async (id: string, field: keyof VendorPoolItem, newValue: any) => {
+ const isEmptyRow = String(id).startsWith('temp-')
+
+ if (isEmptyRow) {
+ // 빈 행의 경우 emptyRows 상태도 업데이트
+ setEmptyRows(prev => ({
+ ...prev,
+ [id]: {
+ ...prev[id],
+ [field]: newValue
+ }
+ }))
+ }
+
+ // pendingChanges에 변경사항 저장 (실시간 표시용)
+ setPendingChanges(prev => ({
+ ...prev,
+ [id]: {
+ ...prev[id],
+ [field]: newValue
+ }
+ }))
+ }, [])
+
+ // 사업자번호 변경 시 자동 vendor 검색 핸들러
+ const handleTaxIdChange = React.useCallback(async (id: string, taxId: string) => {
+ if (!taxId || taxId.trim() === '') return
+
+ try {
+ const result = await getVendorByTaxId(taxId.trim())
+ if (result.data) {
+ // vendor 정보가 있으면 vendorCode와 vendorName을 자동으로 설정
+ await handleCellUpdate(id, 'vendorCode', result.data.vendorCode || '')
+ await handleCellUpdate(id, 'vendorName', result.data.vendorName || '')
+ toast.success(`사업자번호로 '${result.data.vendorName}' 업체 정보를 자동 입력했습니다.`)
+ } else {
+ // vendor 정보가 없으면 vendorCode와 vendorName을 빈 값으로 설정
+ await handleCellUpdate(id, 'vendorCode', '')
+ await handleCellUpdate(id, 'vendorName', '')
+ }
+ } catch (error) {
+ console.error('사업자번호 검색 실패:', error)
+ toast.error('사업자번호 검색 중 오류가 발생했습니다.')
+ }
+ }, [handleCellUpdate])
+
+ // 자재그룹코드 변경 시 자동 materialGroupName 검색 및 Equip/Bulk 구분 설정 핸들러
+ const handleMaterialGroupCodeChange = React.useCallback(async (id: string, materialGroupCode: string) => {
+ if (!materialGroupCode || materialGroupCode.trim() === '') return
+
+ const code = materialGroupCode.trim()
+
+ try {
+ const materialGroup = await getMaterialGroupDetail(code)
+ if (materialGroup) {
+ // 자재그룹 정보가 있으면 materialGroupName을 자동으로 설정
+ await handleCellUpdate(id, 'materialGroupName', materialGroup.materialGroupDesc || '')
+ toast.success(`자재그룹코드로 '${materialGroup.materialGroupDesc}' 정보를 자동 입력했습니다.`)
+ } else {
+ // 자재그룹 정보가 없으면 materialGroupName을 빈 값으로 설정
+ await handleCellUpdate(id, 'materialGroupName', '')
+ }
+
+ // Equip/Bulk 구분 자동 설정
+ let equipBulkDivision = ''
+ if (code.startsWith('A1')) {
+ equipBulkDivision = 'S'
+ } else if (code.startsWith('A') || code.startsWith('B7') || code === 'SP1328' || code === 'SP1329') {
+ equipBulkDivision = 'B'
+ } else if (code.startsWith('B')) {
+ equipBulkDivision = 'E'
+ } else {
+ equipBulkDivision = null
+ }
+
+ if (equipBulkDivision) {
+ await handleCellUpdate(id, 'equipBulkDivision', equipBulkDivision)
+ toast.success(`자재그룹코드에 따라 Equip/Bulk 구분을 '${equipBulkDivision}'으로 자동 설정했습니다.`)
+ } else {
+ toast.info('현 자재그룹코드에 따라 Equip/Bulk 구분을 자동 설정할 수 없습니다.')
+ }
+ } catch (error) {
+ console.error('자재그룹코드 검색 실패:', error)
+ toast.error('자재그룹코드 검색 중 오류가 발생했습니다.')
+ }
+ }, [handleCellUpdate])
+
+
+ // 편집 취소 핸들러
+ const handleCellCancel = React.useCallback((id: string, field: keyof VendorPoolItem) => {
+ const isEmptyRow = String(id).startsWith('temp-')
+
+ if (isEmptyRow) {
+ // 빈 행의 경우 emptyRows와 pendingChanges 모두 취소
+ setEmptyRows(prev => ({
+ ...prev,
+ [id]: {
+ ...prev[id],
+ [field]: prev[id][field] // 원래 값으로 복원 (pendingChanges의 초기값 사용)
+ }
+ }))
+
+ setPendingChanges(prev => {
+ const itemChanges = { ...prev[id] }
+ delete itemChanges[field]
+
+ if (Object.keys(itemChanges).length === 0) {
+ const newChanges = { ...prev }
+ delete newChanges[id]
+ return newChanges
+ }
+
+ return {
+ ...prev,
+ [id]: itemChanges
+ }
+ })
+ } else {
+ // 기존 데이터의 경우
+ setPendingChanges(prev => {
+ const itemChanges = { ...prev[id] }
+ delete itemChanges[field]
+
+ if (Object.keys(itemChanges).length === 0) {
+ const newChanges = { ...prev }
+ delete newChanges[id]
+ return newChanges
+ }
+
+ return {
+ ...prev,
+ [id]: itemChanges
+ }
+ })
+ }
+ }, [])
+
+ // 일괄 저장 핸들러
+ const handleBatchSave = React.useCallback(async () => {
+ if (Object.keys(pendingChanges).length === 0) return
+
+ setIsSaving(true)
+ let successCount = 0
+ let errorCount = 0
+
+ try {
+ // 각 항목의 변경사항을 순차적으로 저장
+ for (const [id, changes] of Object.entries(pendingChanges)) {
+ try {
+ // changes에서 id 필드 제거 (서버에서 자동 생성)
+ const { id: _, ...updateData } = changes as any
+ // 최종변경자를 현재 세션 사용자 정보로 설정
+ const updateDataWithModifier = {
+ ...updateData,
+ lastModifier: session?.user?.name || ""
+ }
+ const result = await updateVendorPool(Number(id), updateDataWithModifier as Partial<VendorPool>)
+ if (result) {
+ successCount++
+ } else {
+ errorCount++
+ }
+ } catch (error) {
+ console.error(`항목 ${id} 저장 실패:`, error)
+ errorCount++
+ }
+ }
+
+ // 저장 완료 후 pendingChanges 초기화
+ setPendingChanges({})
+
+ if (successCount > 0) {
+ toast.success(`${successCount}개 항목이 저장되었습니다.`)
+
+ // 편집 모드 종료를 위해 테이블 상태 리셋 및 데이터 새로고침
+ table.resetRowSelection()
+ onRefresh?.() // 데이터 새로고침
+ }
+
+ if (errorCount > 0) {
+ toast.error(`${errorCount}개 항목 저장에 실패했습니다.`)
+ }
+ } catch (error) {
+ console.error("Batch save error:", error)
+ toast.error("저장 중 오류가 발생했습니다.")
+ } finally {
+ setIsSaving(false)
+ }
+ }, [pendingChanges, onRefresh])
+
+ // 수정사항 존재 여부
+ const hasPendingChanges = Object.keys(pendingChanges).length > 0
+
+ // 빈 행 생성 함수
+ const createEmptyRow = React.useCallback(() => {
+ if (isCreating) return // 이미 생성 중이면 중복 생성 방지
+
+ const tempId = `temp-${Date.now()}`
+ const emptyRow: VendorPoolItem = {
+ id: tempId,
+ no: 0, // 나중에 계산
+ selected: false,
+ constructionSector: "",
+ htDivision: "",
+ designCategoryCode: "",
+ designCategory: "",
+ equipBulkDivision: "",
+ packageCode: "",
+ packageName: "",
+ materialGroupCode: "",
+ materialGroupName: "",
+ smCode: "",
+ similarMaterialNamePurchase: "",
+ similarMaterialNameOther: "",
+ vendorCode: "",
+ vendorName: "",
+ taxId: "",
+ faTarget: false,
+ faStatus: "",
+ faRemark: "",
+ tier: "",
+ isAgent: false,
+ contractSignerCode: "",
+ contractSignerName: "",
+ headquarterLocation: "",
+ manufacturingLocation: "",
+ avlVendorName: "",
+ similarVendorName: "",
+ hasAvl: false,
+ isBlacklist: false,
+ isBcc: false,
+ purchaseOpinion: "",
+ shipTypeCommon: false,
+ shipTypeAmax: false,
+ shipTypeSmax: false,
+ shipTypeVlcc: false,
+ shipTypeLngc: false,
+ shipTypeCont: false,
+ offshoreTypeCommon: false,
+ offshoreTypeFpso: false,
+ offshoreTypeFlng: false,
+ offshoreTypeFpu: false,
+ offshoreTypePlatform: false,
+ offshoreTypeWtiv: false,
+ offshoreTypeGom: false,
+ picName: "",
+ picEmail: "",
+ picPhone: "",
+ agentName: "",
+ agentEmail: "",
+ agentPhone: "",
+ recentQuoteDate: "",
+ recentQuoteNumber: "",
+ recentOrderDate: "",
+ recentOrderNumber: "",
+ registrationDate: "",
+ registrant: session?.user?.name || "",
+ lastModifiedDate: "",
+ lastModifier: session?.user?.name || "",
+ }
+
+ setEmptyRows(prev => ({ ...prev, [tempId]: emptyRow }))
+ setIsCreating(true)
+
+
+ // 빈 행의 초기값들을 pendingChanges에 설정 (임시 저장용)
+ // emptyRow의 실제 값들을 반영
+ setPendingChanges(prev => ({
+ ...prev,
+ [tempId]: { ...emptyRow }
+ }))
+ }, [isCreating])
+
+ // 빈 행 저장 함수
+ const saveEmptyRow = React.useCallback(async (tempId: string) => {
+ const rowData = emptyRows[tempId]
+ const changes = pendingChanges[tempId]
+
+
+ if (!rowData || !changes) {
+ console.error('rowData 또는 changes가 없음')
+ return
+ }
+
+ // emptyRows와 pendingChanges를 병합한 최종 데이터
+ const finalData = { ...rowData, ...changes }
+
+ // 필수 필드 검증 (최종 데이터 기준)
+ const requiredFields = ['constructionSector', 'htDivision', 'designCategory', 'taxId', 'vendorName', 'materialGroupCode', 'materialGroupName', 'tier', 'contractSignerName', 'headquarterLocation', 'manufacturingLocation', 'avlVendorName']
+
+ // 필드명과 한국어 레이블 매핑
+ const fieldLabels: Record<string, string> = {
+ constructionSector: '공사부문',
+ htDivision: 'H/T구분',
+ designCategory: '설계기능',
+ taxId: '사업자번호',
+ vendorName: '협력업체명',
+ materialGroupCode: '자재그룹코드',
+ materialGroupName: '자재그룹명',
+ tier: '등급(Tier)',
+ contractSignerName: '계약서명주체명',
+ headquarterLocation: '위치(국가)',
+ manufacturingLocation: '제작/선적지(국가)',
+ avlVendorName: 'AVL등재업체명'
+ }
+
+ const missingFields = requiredFields.filter(field => {
+ const value = finalData[field as keyof VendorPoolItem]
+ return !value || value === ''
+ })
+
+ if (missingFields.length > 0) {
+ const missingFieldLabels = missingFields.map(field => fieldLabels[field]).join(', ')
+ toast.error(`필수 항목을 입력해주세요: ${missingFieldLabels}`)
+ return
+ }
+
+ try {
+ setIsSaving(true)
+
+ // id 필드 제거 (서버에서 자동 생성)
+ const { id: _, no: __, selected: ___, ...createData } = finalData
+
+ const result = await createVendorPool(createData as Omit<VendorPool, 'id' | 'registrationDate' | 'lastModifiedDate'>)
+
+ if (result) {
+ toast.success("새 항목이 추가되었습니다.")
+
+ // 빈 행 제거
+ setEmptyRows(prev => {
+ const newRows = { ...prev }
+ delete newRows[tempId]
+ return newRows
+ })
+
+ setPendingChanges(prev => {
+ const newChanges = { ...prev }
+ delete newChanges[tempId]
+ return newChanges
+ })
+
+ setIsCreating(false)
+
+ // 데이터 새로고침
+ onRefresh?.()
+ }
+ } catch (error) {
+ console.error("빈 행 저장 실패:", error)
+ toast.error("저장 중 오류가 발생했습니다.")
+ } finally {
+ setIsSaving(false)
+ }
+ }, [emptyRows, pendingChanges, onRefresh])
+
+ // 빈 행 취소 함수
+ const cancelEmptyRow = React.useCallback((tempId: string) => {
+ setEmptyRows(prev => {
+ const newRows = { ...prev }
+ delete newRows[tempId]
+ return newRows
+ })
+
+ setPendingChanges(prev => {
+ const newChanges = { ...prev }
+ delete newChanges[tempId]
+ return newChanges
+ })
+
+ setIsCreating(false)
+ toast.info("새 항목 추가가 취소되었습니다.")
+ }, [])
+
+
+ // 필터 필드 정의
+ const filterFields: DataTableFilterField<VendorPoolItem>[] = [
+ {
+ id: "constructionSector",
+ label: "공사부문",
+ options: [
+ { label: "조선", value: "조선" },
+ { label: "해양", value: "해양" },
+ ]
+ },
+ {
+ id: "htDivision",
+ label: "H/T구분",
+ options: [
+ { label: "H", value: "H" },
+ { label: "T", value: "T" },
+ { label: "공통", value: "공통" },
+ ]
+ },
+ {
+ id: "equipBulkDivision",
+ label: "Equip/Bulk 구분",
+ options: [
+ { label: "E (Equip)", value: "E" },
+ { label: "B (Bulk)", value: "B" },
+ ]
+ },
+ {
+ id: "faTarget",
+ label: "FA대상",
+ options: [
+ { label: "대상", value: "true" },
+ { label: "비대상", value: "false" },
+ ]
+ },
+ {
+ id: "isAgent",
+ label: "Agent 여부",
+ options: [
+ { label: "Agent", value: "true" },
+ { label: "일반", value: "false" },
+ ]
+ },
+ {
+ id: "hasAvl",
+ label: "AVL 존재",
+ options: [
+ { label: "있음", value: "true" },
+ { label: "없음", value: "false" },
+ ]
+ },
+ {
+ id: "isBlacklist",
+ label: "Blacklist",
+ options: [
+ { label: "등록됨", value: "true" },
+ { label: "미등록", value: "false" },
+ ]
+ },
+ {
+ id: "isBcc",
+ label: "BCC",
+ options: [
+ { label: "등록됨", value: "true" },
+ { label: "미등록", value: "false" },
+ ]
+ }
+ ]
+
+ const advancedFilterFields: DataTableAdvancedFilterField<VendorPoolItem>[] = [
+ {
+ id: "designCategoryCode",
+ label: "설계기능코드",
+ type: "text",
+ },
+ {
+ id: "designCategory",
+ label: "설계기능(공종)",
+ type: "text",
+ },
+ {
+ id: "packageCode",
+ label: "패키지 코드",
+ type: "text",
+ },
+ {
+ id: "packageName",
+ label: "패키지 명",
+ type: "text",
+ },
+ {
+ id: "materialGroupCode",
+ label: "자재그룹 코드",
+ type: "text",
+ },
+ {
+ id: "materialGroupName",
+ label: "자재그룹 명",
+ type: "text",
+ },
+ {
+ id: "vendorCode",
+ label: "협력업체 코드",
+ type: "text",
+ },
+ {
+ id: "vendorName",
+ label: "협력업체 명",
+ type: "text",
+ },
+ {
+ id: "taxId",
+ label: "사업자번호",
+ type: "text",
+ },
+ {
+ id: "faStatus",
+ label: "FA현황",
+ type: "text",
+ },
+ {
+ id: "tier",
+ label: "등급",
+ type: "text",
+ },
+ {
+ id: "headquarterLocation",
+ label: "본사 위치",
+ type: "text",
+ },
+ {
+ id: "manufacturingLocation",
+ label: "제작/선적지",
+ type: "text",
+ },
+ {
+ id: "avlVendorName",
+ label: "AVL 등재업체명",
+ type: "text",
+ },
+ {
+ id: "registrant",
+ label: "등재자",
+ type: "text",
+ },
+ {
+ id: "lastModifier",
+ label: "최종변경자",
+ type: "text",
+ },
+ {
+ id: "registrationDate",
+ label: "등재일",
+ type: "date",
+ },
+ {
+ id: "lastModifiedDate",
+ label: "최종변경일",
+ type: "date",
+ },
+ ]
+
+ // 빈 행들을 기존 데이터와 합치기 (빈 행들을 최상단에 배치)
+ const combinedData = React.useMemo(() => {
+ const existingData = [...data]
+ const emptyRowList = Object.values(emptyRows)
+
+ // 빈 행들의 no 필드 업데이트 (음수로 설정하여 최상단에 배치)
+ const updatedEmptyRows = emptyRowList.map((row, index) => ({
+ ...row,
+ no: -(emptyRowList.length - index) // -3, -2, -1 순으로 설정
+ }))
+
+ // 기존 데이터의 no 필드도 1부터 재설정
+ const updatedExistingData = existingData.map((row, index) => {
+ const originalRow = existingData[index]
+ const rowId = String(originalRow.id)
+ const pendingChange = pendingChanges[rowId]
+
+ // pendingChanges의 값으로 데이터 병합
+ const mergedRow = pendingChange ? { ...originalRow, ...pendingChange } : originalRow
+
+ return {
+ ...mergedRow,
+ no: index + 1
+ }
+ })
+
+ // 빈 행들을 최상단에 배치
+ return [...updatedEmptyRows, ...updatedExistingData]
+ }, [data, emptyRows, pendingChanges])
+
+ const { table } = useDataTable({
+ data: combinedData,
+ columns,
+ pageCount: pageCount || 0,
+ filterFields,
+ enablePinning: true,
+ enableAdvancedFilter: true,
+ enableColumnResizing: true,
+ columnResizeMode: "onChange",
+ initialState: {
+ sorting: [{ id: "registrationDate", desc: true }],
+ columnPinning: { right: ["actions"] },
+ },
+ getRowId: (originalRow) => String(originalRow.id),
+ shallow: false,
+ clearOnDefault: true,
+ })
+
+ // 액션 핸들러
+ const handleAction = React.useCallback(async (action: string, data?: any) => {
+ try {
+ switch (action) {
+ case 'new-registration':
+ createEmptyRow()
+ break
+
+ case 'bulk-import':
+ setBulkImportDialogOpen(true)
+ break
+
+ case 'save':
+ toast.info('저장 기능은 개발 중입니다.')
+ break
+
+ case 'fixed-values':
+ toast.info('고정값 설정 기능은 개발 중입니다.')
+ break
+
+ case 'delete':
+ if (data?.id && confirm('정말 삭제하시겠습니까?')) {
+ const success = await deleteVendorPool(Number(data.id))
+ if (success) {
+ toast.success('삭제가 완료되었습니다.')
+ onRefresh?.() // 데이터 새로고침
+ } else {
+ toast.error('삭제에 실패했습니다.')
+ }
+ }
+ break
+
+ default:
+ console.log('알 수 없는 액션:', action)
+ toast.error('알 수 없는 액션입니다.')
+ }
+ } catch (error) {
+ console.error('액션 처리 실패:', error)
+ toast.error('액션 처리 중 오류가 발생했습니다.')
+ }
+ }, [table, onRefresh])
+
+ // 일괄입력 핸들러
+ const handleBulkImport = React.useCallback(async (bulkData: Record<string, any>) => {
+ const selectedRows = table.getFilteredSelectedRowModel().rows
+
+ if (selectedRows.length === 0) {
+ toast.error('일괄 입력할 행이 선택되지 않았습니다.')
+ return
+ }
+
+ try {
+ // 선택된 각 행에 대해 값 적용
+ for (const row of selectedRows) {
+ const rowId = String(row.original.id)
+
+ // 제공된 값들만 적용 (빈 값이나 undefined는 건너뜀)
+ Object.entries(bulkData).forEach(([field, value]) => {
+ if (value !== undefined && value !== null && value !== '') {
+ handleCellUpdate(rowId, field as keyof VendorPoolItem, value)
+ }
+ })
+ }
+
+ toast.success(`${selectedRows.length}개 행에 일괄 입력이 적용되었습니다.`)
+ setBulkImportDialogOpen(false)
+ } catch (error) {
+ console.error('일괄입력 처리 실패:', error)
+ toast.error('일괄입력 처리 중 오류가 발생했습니다.')
+ }
+ }, [table, handleCellUpdate])
+
+ // 테이블 메타에 핸들러 설정
+ table.options.meta = {
+ onAction: handleAction,
+ onCellUpdate: handleCellUpdate,
+ onCellCancel: handleCellCancel,
+ onSaveEmptyRow: saveEmptyRow,
+ onCancelEmptyRow: cancelEmptyRow,
+ isEmptyRow: (id: string) => String(id).startsWith('temp-'),
+ getPendingChanges: () => pendingChanges,
+ onTaxIdChange: handleTaxIdChange,
+ onMaterialGroupCodeChange: handleMaterialGroupCodeChange
+ }
+
+
+ // 툴바 액션 핸들러들
+ const handleToolbarAction = React.useCallback((action: string, data?: any) => {
+ handleAction(action, data)
+ }, [handleAction])
+
+ // 저장 버튼 핸들러
+ const handleSaveChanges = React.useCallback(() => {
+ handleBatchSave()
+ }, [handleBatchSave])
+
+ return (
+ <>
+ <DataTable
+ table={table}
+ className="[&_[data-row-id^='temp-']]:bg-blue-50 [&_[data-row-id^='temp-']]:border-blue-200"
+ // autoSizeColumns={true}
+ >
+ <DataTableAdvancedToolbar
+ table={table}
+ filterFields={advancedFilterFields}
+ shallow={false}
+ >
+ <div className="flex items-center gap-2">
+ <Button
+ onClick={() => handleToolbarAction('new-registration')}
+ disabled={isCreating}
+ variant="outline"
+ size="sm"
+ >
+ 신규등록
+ </Button>
+
+ <Button
+ onClick={() => handleToolbarAction('bulk-import')}
+ variant="outline"
+ size="sm"
+ >
+ 일괄입력
+ </Button>
+
+ <Button
+ onClick={() => handleToolbarAction('fixed-values')}
+ variant="outline"
+ size="sm"
+ >
+ FA 상세
+ </Button>
+
+ <Button
+ onClick={handleSaveChanges}
+ disabled={!hasPendingChanges || isSaving}
+ variant={hasPendingChanges && !isSaving ? "default" : "outline"}
+ size="sm"
+ >
+ {isSaving ? "저장 중..." : `저장${hasPendingChanges ? ` (${Object.keys(pendingChanges).length})` : ""}`}
+ </Button>
+
+ {/* <Button
+ onClick={() => handleToolbarAction('fixed-values')}
+ variant="outline"
+ size="sm"
+ >
+ 고정값 설정
+ </Button> */}
+ </div>
+ </DataTableAdvancedToolbar>
+ </DataTable>
+
+ <BulkImportDialog
+ open={bulkImportDialogOpen}
+ onOpenChange={setBulkImportDialogOpen}
+ onSubmit={handleBulkImport}
+ />
+ </>
+ )
+}