summaryrefslogtreecommitdiff
path: root/lib/vendor-pool/table
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendor-pool/table')
-rw-r--r--lib/vendor-pool/table/bulk-import-dialog.tsx242
-rw-r--r--lib/vendor-pool/table/columns.tsx1687
-rw-r--r--lib/vendor-pool/table/vendor-pool-table.tsx806
3 files changed, 2735 insertions, 0 deletions
diff --git a/lib/vendor-pool/table/bulk-import-dialog.tsx b/lib/vendor-pool/table/bulk-import-dialog.tsx
new file mode 100644
index 00000000..50c20d08
--- /dev/null
+++ b/lib/vendor-pool/table/bulk-import-dialog.tsx
@@ -0,0 +1,242 @@
+"use client"
+
+import * as React from "react"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { Checkbox } from "@/components/ui/checkbox"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+
+interface BulkImportDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ onSubmit: (data: Record<string, any>) => void
+}
+
+export function BulkImportDialog({ open, onOpenChange, onSubmit }: BulkImportDialogProps) {
+ const [formData, setFormData] = React.useState<Record<string, any>>({
+ equipBulkDivision: "",
+ similarMaterialNamePurchase: "",
+ faTarget: null,
+ tier: "",
+ isAgent: null,
+ headquarterLocation: "",
+ manufacturingLocation: "",
+ avlVendorName: "",
+ isBlacklist: null,
+ isBcc: null,
+ })
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault()
+
+ // 빈 값이나 기본값은 제외하고 실제 변경할 값만 전달
+ const filteredData: Record<string, any> = {}
+
+ Object.entries(formData).forEach(([key, value]) => {
+ if (value !== "" && value !== null && value !== undefined) {
+ filteredData[key] = value
+ }
+ })
+
+ if (Object.keys(filteredData).length === 0) {
+ return
+ }
+
+ onSubmit(filteredData)
+ // 폼 초기화
+ setFormData({
+ equipBulkDivision: "",
+ similarMaterialNamePurchase: "",
+ faTarget: null,
+ tier: "",
+ isAgent: null,
+ headquarterLocation: "",
+ manufacturingLocation: "",
+ avlVendorName: "",
+ isBlacklist: null,
+ isBcc: null,
+ })
+ }
+
+ const handleCancel = () => {
+ setFormData({
+ equipBulkDivision: "",
+ similarMaterialNamePurchase: "",
+ faTarget: null,
+ tier: "",
+ isAgent: null,
+ headquarterLocation: "",
+ manufacturingLocation: "",
+ avlVendorName: "",
+ isBlacklist: null,
+ isBcc: null,
+ })
+ onOpenChange(false)
+ }
+
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="sm:max-w-[500px]">
+ <DialogHeader>
+ <DialogTitle>일괄 입력</DialogTitle>
+ <DialogDescription>
+ 선택된 행들에 동일한 값을 입력합니다. 빈 칸은 변경하지 않습니다.
+ </DialogDescription>
+ </DialogHeader>
+
+ <form onSubmit={handleSubmit} className="space-y-4">
+ <div className="grid grid-cols-2 gap-4">
+ {/* Equip/Bulk 구분 */}
+ <div className="space-y-2">
+ <Label htmlFor="equipBulkDivision">Equip/Bulk 구분</Label>
+ <Select
+ value={formData.equipBulkDivision}
+ onValueChange={(value) => setFormData(prev => ({ ...prev, equipBulkDivision: value }))}
+ >
+ <SelectTrigger>
+ <SelectValue placeholder="선택" />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="E">E (Equip)</SelectItem>
+ <SelectItem value="B">B (Bulk)</SelectItem>
+ <SelectItem value="S">S (강재)</SelectItem>
+ </SelectContent>
+ </Select>
+ </div>
+
+ {/* 유사자재명(구매) */}
+ <div className="space-y-2">
+ <Label htmlFor="similarMaterialNamePurchase">유사자재명(구매)</Label>
+ <Input
+ id="similarMaterialNamePurchase"
+ value={formData.similarMaterialNamePurchase}
+ onChange={(e) => setFormData(prev => ({ ...prev, similarMaterialNamePurchase: e.target.value }))}
+ placeholder="유사자재명 입력"
+ />
+ </div>
+
+ {/* FA대상 */}
+ <div className="space-y-2">
+ <Label>FA대상</Label>
+ <div className="flex items-center space-x-2">
+ <Checkbox
+ id="faTarget"
+ checked={formData.faTarget === true}
+ onCheckedChange={(checked) => setFormData(prev => ({ ...prev, faTarget: checked ? true : null }))}
+ />
+ <Label htmlFor="faTarget" className="text-sm">대상</Label>
+ </div>
+ </div>
+
+ {/* 등급 */}
+ <div className="space-y-2">
+ <Label htmlFor="tier">등급</Label>
+ <Input
+ id="tier"
+ value={formData.tier}
+ onChange={(e) => setFormData(prev => ({ ...prev, tier: e.target.value }))}
+ placeholder="등급 입력"
+ />
+ </div>
+
+ {/* Agent 여부 */}
+ <div className="space-y-2">
+ <Label>Agent 여부</Label>
+ <div className="flex items-center space-x-2">
+ <Checkbox
+ id="isAgent"
+ checked={formData.isAgent === true}
+ onCheckedChange={(checked) => setFormData(prev => ({ ...prev, isAgent: checked ? true : null }))}
+ />
+ <Label htmlFor="isAgent" className="text-sm">Agent</Label>
+ </div>
+ </div>
+
+ {/* 본사위치(국가) */}
+ <div className="space-y-2">
+ <Label htmlFor="headquarterLocation">본사위치(국가)</Label>
+ <Input
+ id="headquarterLocation"
+ value={formData.headquarterLocation}
+ onChange={(e) => setFormData(prev => ({ ...prev, headquarterLocation: e.target.value }))}
+ placeholder="국가명 입력"
+ />
+ </div>
+
+ {/* 제작/선적지(국가) */}
+ <div className="space-y-2">
+ <Label htmlFor="manufacturingLocation">제작/선적지(국가)</Label>
+ <Input
+ id="manufacturingLocation"
+ value={formData.manufacturingLocation}
+ onChange={(e) => setFormData(prev => ({ ...prev, manufacturingLocation: e.target.value }))}
+ placeholder="국가명 입력"
+ />
+ </div>
+
+ {/* AVL등재업체명 */}
+ <div className="space-y-2">
+ <Label htmlFor="avlVendorName">AVL등재업체명</Label>
+ <Input
+ id="avlVendorName"
+ value={formData.avlVendorName}
+ onChange={(e) => setFormData(prev => ({ ...prev, avlVendorName: e.target.value }))}
+ placeholder="업체명 입력"
+ />
+ </div>
+
+ {/* Blacklist */}
+ <div className="space-y-2">
+ <Label>Blacklist</Label>
+ <div className="flex items-center space-x-2">
+ <Checkbox
+ id="isBlacklist"
+ checked={formData.isBlacklist === true}
+ onCheckedChange={(checked) => setFormData(prev => ({ ...prev, isBlacklist: checked ? true : null }))}
+ />
+ <Label htmlFor="isBlacklist" className="text-sm">등록</Label>
+ </div>
+ </div>
+
+ {/* BCC */}
+ <div className="space-y-2">
+ <Label>BCC</Label>
+ <div className="flex items-center space-x-2">
+ <Checkbox
+ id="isBcc"
+ checked={formData.isBcc === true}
+ onCheckedChange={(checked) => setFormData(prev => ({ ...prev, isBcc: checked ? true : null }))}
+ />
+ <Label htmlFor="isBcc" className="text-sm">등록</Label>
+ </div>
+ </div>
+ </div>
+
+ <DialogFooter>
+ <Button type="button" variant="outline" onClick={handleCancel}>
+ 취소
+ </Button>
+ <Button type="submit">
+ 적용
+ </Button>
+ </DialogFooter>
+ </form>
+ </DialogContent>
+ </Dialog>
+ )
+}
diff --git a/lib/vendor-pool/table/columns.tsx b/lib/vendor-pool/table/columns.tsx
new file mode 100644
index 00000000..0a6b0c8f
--- /dev/null
+++ b/lib/vendor-pool/table/columns.tsx
@@ -0,0 +1,1687 @@
+import { Checkbox } from "@/components/ui/checkbox"
+import { Button } from "@/components/ui/button"
+import { MoreHorizontal, Eye, Edit, Trash2 } from "lucide-react"
+import { type ColumnDef, TableMeta } from "@tanstack/react-table"
+import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
+import { EditableCell } from "@/components/data-table/editable-cell"
+
+// 수정 여부 확인 헬퍼 함수
+const getIsModified = (table: any, rowId: string, fieldName: string) => {
+ const pendingChanges = table.options.meta?.getPendingChanges?.() || {}
+ return String(rowId) in pendingChanges && fieldName in pendingChanges[String(rowId)]
+}
+
+// 테이블 메타 타입 확장
+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
+ onTaxIdChange?: (id: string, taxId: string) => Promise<void>
+ onMaterialGroupCodeChange?: (id: string, materialGroupCode: string) => Promise<void>
+ }
+}
+
+// Vendor Pool 데이터 타입
+export type VendorPoolItem = {
+ id: string
+ no: number
+ selected: boolean
+ constructionSector: string // 공사부문: 조선 또는 해양
+ htDivision: string // H/T구분: H 또는 T
+ designCategoryCode: string // 설계기능(공종) 코드: 2자리 영문대문자
+ designCategory: string // 설계기능(공종): 전장 등
+ equipBulkDivision: string // Equip/Bulk 구분: E 또는 B
+ // 패키지 정보 (스키마: packageCode, packageName)
+ packageCode: string
+ packageName: string
+ // 자재그룹 (스키마: materialGroupCode, materialGroupName)
+ materialGroupCode: string
+ materialGroupName: string
+ smCode: string // SM Code
+ similarMaterialNamePurchase: string // 유사자재명 (구매)
+ similarMaterialNameOther: string // 유사자재명 (구매 외)
+ // 협력업체 정보 (스키마: vendorCode, vendorName)
+ vendorCode: string
+ vendorName: string
+ taxId: string // 사업자번호(Tax ID)
+ faTarget: boolean // FA대상
+ faStatus: string // FA현황
+ faRemark: string // FA상세(Remark)
+ tier: string // 등급(Tier)
+ isAgent: boolean // Agent 여부
+ // 계약서명주체 (스키마: contractSignerCode, contractSignerName)
+ contractSignerCode: string
+ contractSignerName: string
+ headquarterLocation: string // 본사 위치(국가)
+ manufacturingLocation: string // 제작/선적지(국가)
+ avlVendorName: string // AVL 등재업체명
+ similarVendorName: string // 유사업체명(기술영업)
+ hasAvl: boolean // AVL: 존재여부
+ isBlacklist: boolean // Blacklist
+ isBcc: boolean // BCC
+ purchaseOpinion: string // 구매의견
+ // AVL 적용 선종(조선)
+ shipTypeCommon: boolean // 공통
+ shipTypeAmax: boolean // A-max
+ shipTypeSmax: boolean // S-max
+ shipTypeVlcc: boolean // VLCC
+ shipTypeLngc: boolean // LNGC
+ shipTypeCont: boolean // CONT
+ // AVL 적용 선종(해양)
+ offshoreTypeCommon: boolean // 공통
+ offshoreTypeFpso: boolean // FPSO
+ offshoreTypeFlng: boolean // FLNG
+ offshoreTypeFpu: boolean // FPU
+ offshoreTypePlatform: boolean // Platform
+ offshoreTypeWtiv: boolean // WTIV
+ offshoreTypeGom: boolean // GOM
+ // eVCP 미등록 정보
+ picName: string // PIC(담당자)
+ picEmail: string // PIC(E-mail)
+ picPhone: string // PIC(Phone)
+ agentName: string // Agent(담당자)
+ agentEmail: string // Agent(E-mail)
+ agentPhone: string // Agent(Phone)
+ // 업체 실적 현황
+ recentQuoteDate: string // 최근견적일
+ recentQuoteNumber: string // 최근견적번호
+ recentOrderDate: string // 최근발주일
+ recentOrderNumber: string // 최근발주번호
+ // 업데이트 히스토리
+ registrationDate: string // 등재일
+ registrant: string // 등재자
+ lastModifiedDate: string // 최종변경일
+ lastModifier: string // 최종변경자
+}
+
+// 테이블 컬럼 정의
+export const columns: ColumnDef<VendorPoolItem>[] = [
+ // 기본 정보 그룹
+ {
+ header: "기본 정보",
+ columns: [
+ {
+ id: "select",
+ header: ({ table }) => (
+ <Checkbox
+ checked={
+ table.getIsAllPageRowsSelected() ||
+ (table.getIsSomePageRowsSelected() && "indeterminate")
+ }
+ onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
+ aria-label="Select all"
+ />
+ ),
+ cell: ({ row }) => (
+ <Checkbox
+ checked={row.getIsSelected()}
+ onCheckedChange={(value) => row.toggleSelected(!!value)}
+ aria-label="Select row"
+ />
+ ),
+ enableSorting: false,
+ enableHiding: false,
+ size: 40,
+ },
+ {
+ accessorKey: "id",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="No." />
+ ),
+ cell: ({ row }) => {
+ const id = String(row.original.id)
+
+ // 빈 행의 경우 No. 표시하지 않음
+ if (id.startsWith('temp-')) {
+ return <div className="text-sm text-muted-foreground italic">신규</div>
+ }
+
+ // vendor_pool 테이블의 실제 id 표시
+ return <div className="text-sm font-mono">{id}</div>
+ },
+ size: 60,
+ },
+ {
+ accessorKey: "constructionSector",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">공사부문 *</span>} />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("constructionSector")
+ const isEmptyRow = String(row.original.id).startsWith('temp-')
+
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "constructionSector", newValue)
+ }
+ }
+
+ // 수정 여부 확인
+ const isModified = getIsModified(table, row.original.id, "constructionSector")
+
+ return (
+ <EditableCell
+ value={value}
+ type="select"
+ onSave={onSave}
+ options={[
+ { label: "조선", value: "조선" },
+ { label: "해양", value: "해양" }
+ ]}
+ placeholder="공사부문 선택"
+ autoSave={true}
+ disabled={false}
+ initialEditMode={isEmptyRow}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 100,
+ },
+ {
+ accessorKey: "htDivision",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">H/T구분 *</span>} />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("htDivision")
+ const isEmptyRow = String(row.original.id).startsWith('temp-')
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "htDivision", newValue)
+ }
+ }
+
+ // 수정 여부 확인
+ const isModified = getIsModified(table, row.original.id, "htDivision")
+
+ return (
+ <EditableCell
+ value={value}
+ type="select"
+ onSave={onSave}
+ options={[
+ { label: "H", value: "H" },
+ { label: "T", value: "T" },
+ { label: "공통", value: "공통" },
+ ]}
+ placeholder="H/T 선택"
+ autoSave={false}
+ initialEditMode={isEmptyRow}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 80,
+ },
+ {
+ accessorKey: "designCategoryCode",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="설계기능코드" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("designCategoryCode")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "designCategoryCode", newValue)
+ }
+ }
+
+ // 수정 여부 확인
+ const isModified = getIsModified(table, row.original.id, "designCategoryCode")
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="설계기능코드 입력"
+ maxLength={10}
+ autoSave={false}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "designCategory",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">설계기능(공종) *</span>} />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("designCategory")
+ const isEmptyRow = String(row.original.id).startsWith('temp-')
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "designCategory", newValue)
+ }
+ }
+
+ // 수정 여부 확인
+ const isModified = getIsModified(table, row.original.id, "designCategory")
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="설계기능(공종) 입력"
+ maxLength={50}
+ autoSave={false}
+ initialEditMode={isEmptyRow}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "equipBulkDivision",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="Equip/Bulk 구분" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("equipBulkDivision")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "equipBulkDivision", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="select"
+ onSave={onSave}
+ options={[
+ { label: "E (Equip)", value: "E" },
+ { label: "B (Bulk)", value: "B" },
+ { label: "S (강재)", value: "S" }
+ ]}
+ placeholder="구분 선택"
+ autoSave={false}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "packageCode",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="패키지 코드" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("packageCode")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "packageCode", newValue)
+ }
+ }
+
+ // 수정 여부 확인
+ const isModified = getIsModified(table, row.original.id, "packageCode")
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="패키지 코드 입력"
+ maxLength={50}
+ autoSave={false}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "packageName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="패키지 명" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("packageName")
+ const isEmptyRow = String(row.original.id).startsWith('temp-')
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "packageName", newValue)
+ }
+ }
+
+ // 수정 여부 확인
+ const isModified = getIsModified(table, row.original.id, "packageName")
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="패키지 명 입력"
+ maxLength={100}
+ autoSave={false}
+ initialEditMode={isEmptyRow}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "materialGroupCode",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">자재그룹 코드 *</span>} />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("materialGroupCode")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "materialGroupCode", newValue)
+ }
+ }
+
+ // 수정 여부 확인
+ const isModified = getIsModified(table, row.original.id, "materialGroupCode")
+
+ const onChange = async (newValue: any) => {
+ if (table.options.meta?.onMaterialGroupCodeChange) {
+ await table.options.meta.onMaterialGroupCodeChange(row.original.id, newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ onChange={onChange}
+ placeholder="자재그룹 코드 입력"
+ maxLength={50}
+ autoSave={false}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "materialGroupName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">자재그룹 명 *</span>} />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("materialGroupName")
+ const isEmptyRow = String(row.original.id).startsWith('temp-')
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "materialGroupName", newValue)
+ }
+ }
+
+ // 수정 여부 확인
+ const isModified = getIsModified(table, row.original.id, "materialGroupName")
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="자재그룹 명 입력"
+ maxLength={100}
+ autoSave={false}
+ initialEditMode={isEmptyRow}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "smCode",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="SM Code" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("smCode")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "smCode", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="SM Code 입력"
+ maxLength={50}
+ />
+ )
+ },
+ size: 100,
+ },
+ ]
+ },
+ // 자재 정보 그룹
+ {
+ header: "자재 정보",
+ columns: [
+ {
+ accessorKey: "similarMaterialNamePurchase",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="유사자재명(구매)" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("similarMaterialNamePurchase")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "similarMaterialNamePurchase", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="유사자재명(구매) 입력"
+ maxLength={100}
+ autoSave={false}
+ />
+ )
+ },
+ size: 140,
+ },
+ {
+ accessorKey: "similarMaterialNameOther",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="유사자재명(구매외)" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("similarMaterialNameOther")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "similarMaterialNameOther", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="유사자재명(구매외) 입력"
+ maxLength={100}
+ autoSave={false}
+ />
+ )
+ },
+ size: 140,
+ },
+ ]
+ },
+ // 협력업체 정보 그룹
+ {
+ header: "협력업체 정보",
+ columns: [
+ {
+ accessorKey: "vendorCode",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="협력업체 코드" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("vendorCode")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "vendorCode", newValue)
+ }
+ }
+
+ // 수정 여부 확인
+ const isModified = getIsModified(table, row.original.id, "vendorCode")
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="협력업체 코드 입력"
+ maxLength={50}
+ autoSave={false}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 130,
+ },
+ {
+ accessorKey: "vendorName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">협력업체 명 *</span>} />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("vendorName")
+ const isEmptyRow = String(row.original.id).startsWith('temp-')
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "vendorName", newValue)
+ }
+ }
+
+ // 수정 여부 확인
+ const isModified = getIsModified(table, row.original.id, "vendorName")
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="협력업체 명 입력"
+ maxLength={100}
+ autoSave={false}
+ initialEditMode={isEmptyRow}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 130,
+ },
+ {
+ accessorKey: "taxId",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">사업자번호 *</span>} />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("taxId")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "taxId", newValue)
+ }
+ }
+
+ const onChange = async (newValue: any) => {
+ if (table.options.meta?.onTaxIdChange) {
+ await table.options.meta.onTaxIdChange(row.original.id, newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ onChange={onChange}
+ placeholder="사업자번호 입력"
+ maxLength={50}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "faTarget",
+ header: "FA대상",
+ cell: ({ row, table }) => {
+ const value = row.getValue("faTarget") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "faTarget", newValue)
+ }
+ }
+
+ // 수정 여부 확인
+ const isModified = getIsModified(table, row.original.id, "faTarget")
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ isModified={isModified}
+ />
+ )
+ },
+ enableSorting: false,
+ size: 80,
+ },
+ {
+ accessorKey: "faStatus",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="FA현황" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("faStatus")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "faStatus", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="FA현황 입력"
+ maxLength={50}
+ />
+ )
+ },
+ size: 100,
+ },
+ {
+ accessorKey: "faRemark",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="FA상세" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("faRemark")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "faRemark", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="textarea"
+ onSave={onSave}
+ placeholder="FA상세 입력"
+ maxLength={500}
+ autoSave={false}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "tier",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">등급 *</span>} />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("tier")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "tier", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="select"
+ onSave={onSave}
+ options={[
+ { label: "Tier 1", value: "Tier 1" },
+ { label: "Tier 2", value: "Tier 2" },
+ { label: "Tier 3", value: "Tier 3" },
+ { label: "Tier 4", value: "Tier 4" },
+ ]}
+ placeholder="등급 선택"
+ />
+ )
+ },
+ size: 80,
+ },
+ {
+ accessorKey: "isAgent",
+ header: "Agent 여부",
+ cell: ({ row, table }) => {
+ const value = row.getValue("isAgent") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "isAgent", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ enableSorting: false,
+ size: 100,
+ },
+ {
+ accessorKey: "contractSignerCode",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="계약서명주체 코드" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("contractSignerCode")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "contractSignerCode", newValue)
+ }
+ }
+
+ // 수정 여부 확인
+ const isModified = getIsModified(table, row.original.id, "contractSignerCode")
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="계약서명주체 코드 입력"
+ maxLength={50}
+ autoSave={false}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "contractSignerName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">계약서명주체 명 *</span>} />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("contractSignerName")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "contractSignerName", newValue)
+ }
+ }
+
+ // 수정 여부 확인
+ const isModified = getIsModified(table, row.original.id, "contractSignerName")
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="계약서명주체 명 입력"
+ maxLength={100}
+ autoSave={false}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "headquarterLocation",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">본사 위치 *</span>} />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("headquarterLocation")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "headquarterLocation", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="본사 위치 입력"
+ maxLength={50}
+ />
+ )
+ },
+ size: 100,
+ },
+ {
+ accessorKey: "manufacturingLocation",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">제작/선적지 *</span>} />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("manufacturingLocation")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "manufacturingLocation", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="제작/선적지 입력"
+ maxLength={50}
+ />
+ )
+ },
+ size: 110,
+ },
+ {
+ accessorKey: "avlVendorName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">AVL 등재업체명 *</span>} />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("avlVendorName")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "avlVendorName", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="AVL 등재업체명 입력"
+ maxLength={100}
+ />
+ )
+ },
+ size: 140,
+ },
+ {
+ accessorKey: "similarVendorName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="유사업체명" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("similarVendorName")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "similarVendorName", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="유사업체명 입력"
+ maxLength={100}
+ />
+ )
+ },
+ size: 130,
+ },
+ {
+ accessorKey: "hasAvl",
+ header: "AVL",
+ cell: ({ row, table }) => {
+ const value = row.getValue("hasAvl") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "hasAvl", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ enableSorting: false,
+ size: 60,
+ },
+ {
+ accessorKey: "isBlacklist",
+ header: "Blacklist",
+ cell: ({ row, table }) => {
+ const value = row.getValue("isBlacklist") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "isBlacklist", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ enableSorting: false,
+ size: 100,
+ },
+ {
+ accessorKey: "isBcc",
+ header: "BCC",
+ cell: ({ row, table }) => {
+ const value = row.getValue("isBcc") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "isBcc", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ enableSorting: false,
+ size: 80,
+ },
+ {
+ accessorKey: "purchaseOpinion",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="구매의견" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("purchaseOpinion")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "purchaseOpinion", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="textarea"
+ onSave={onSave}
+ placeholder="구매의견 입력"
+ maxLength={500}
+ />
+ )
+ },
+ size: 120,
+ },
+ ]
+ },
+ // AVL 적용 선종(조선) 그룹
+ {
+ header: "AVL 적용 선종(조선)",
+ columns: [
+ {
+ accessorKey: "shipTypeCommon",
+ header: "공통",
+ cell: ({ row, table }) => {
+ const value = row.getValue("shipTypeCommon") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "shipTypeCommon", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ enableSorting: false,
+ size: 80,
+ },
+ {
+ accessorKey: "shipTypeAmax",
+ header: "A-max",
+ cell: ({ row, table }) => {
+ const value = row.getValue("shipTypeAmax") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "shipTypeAmax", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ enableSorting: false,
+ size: 80,
+ },
+ {
+ accessorKey: "shipTypeSmax",
+ header: "S-max",
+ cell: ({ row, table }) => {
+ const value = row.getValue("shipTypeSmax") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "shipTypeSmax", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ enableSorting: false,
+ size: 80,
+ },
+ {
+ accessorKey: "shipTypeVlcc",
+ header: "VLCC",
+ cell: ({ row, table }) => {
+ const value = row.getValue("shipTypeVlcc") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "shipTypeVlcc", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ enableSorting: false,
+ size: 80,
+ },
+ {
+ accessorKey: "shipTypeLngc",
+ header: "LNGC",
+ cell: ({ row, table }) => {
+ const value = row.getValue("shipTypeLngc") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "shipTypeLngc", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ size: 80,
+ },
+ {
+ accessorKey: "shipTypeCont",
+ header: "CONT",
+ cell: ({ row, table }) => {
+ const value = row.getValue("shipTypeCont") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "shipTypeCont", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ size: 80,
+ },
+ ]
+ },
+ // AVL 적용 선종(해양) 그룹
+ {
+ header: "AVL 적용 선종(해양)",
+ columns: [
+ {
+ accessorKey: "offshoreTypeCommon",
+ header: "공통",
+ cell: ({ row, table }) => {
+ const value = row.getValue("offshoreTypeCommon") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeCommon", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ size: 80,
+ },
+ {
+ accessorKey: "offshoreTypeFpso",
+ header: "FPSO",
+ cell: ({ row, table }) => {
+ const value = row.getValue("offshoreTypeFpso") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeFpso", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ size: 80,
+ },
+ {
+ accessorKey: "offshoreTypeFlng",
+ header: "FLNG",
+ cell: ({ row, table }) => {
+ const value = row.getValue("offshoreTypeFlng") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeFlng", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ size: 80,
+ },
+ {
+ accessorKey: "offshoreTypeFpu",
+ header: "FPU",
+ cell: ({ row, table }) => {
+ const value = row.getValue("offshoreTypeFpu") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeFpu", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ size: 80,
+ },
+ {
+ accessorKey: "offshoreTypePlatform",
+ header: "Platform",
+ cell: ({ row, table }) => {
+ const value = row.getValue("offshoreTypePlatform") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "offshoreTypePlatform", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ size: 100,
+ },
+ {
+ accessorKey: "offshoreTypeWtiv",
+ header: "WTIV",
+ cell: ({ row, table }) => {
+ const value = row.getValue("offshoreTypeWtiv") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeWtiv", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ size: 80,
+ },
+ {
+ accessorKey: "offshoreTypeGom",
+ header: "GOM",
+ cell: ({ row, table }) => {
+ const value = row.getValue("offshoreTypeGom") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeGom", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ size: 80,
+ },
+ ]
+ },
+ // eVCP 미등록 정보 그룹
+ {
+ header: "eVCP 미등록 정보",
+ columns: [
+ {
+ accessorKey: "picName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="PIC(담당자)" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("picName")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "picName", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="PIC 담당자명 입력"
+ maxLength={50}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "picEmail",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="PIC(E-mail)" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("picEmail")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "picEmail", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="PIC 이메일 입력"
+ maxLength={100}
+ />
+ )
+ },
+ size: 140,
+ },
+ {
+ accessorKey: "picPhone",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="PIC(Phone)" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("picPhone")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "picPhone", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="PIC 전화번호 입력"
+ maxLength={20}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "agentName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="Agent(담당자)" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("agentName")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "agentName", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="Agent 담당자명 입력"
+ maxLength={50}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "agentEmail",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="Agent(E-mail)" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("agentEmail")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "agentEmail", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="Agent 이메일 입력"
+ maxLength={100}
+ />
+ )
+ },
+ size: 140,
+ },
+ {
+ accessorKey: "agentPhone",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="Agent(Phone)" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("agentPhone")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "agentPhone", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="Agent 전화번호 입력"
+ maxLength={20}
+ />
+ )
+ },
+ size: 120,
+ },
+ ]
+ },
+ // 업체 실적 현황 그룹
+ {
+ header: "업체 실적 현황",
+ columns: [
+ {
+ accessorKey: "recentQuoteDate",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="최근견적일" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("recentQuoteDate")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "recentQuoteDate", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="최근견적일 입력 (YYYY-MM-DD)"
+ maxLength={20}
+ autoSave={false}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "recentQuoteNumber",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="최근견적번호" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("recentQuoteNumber")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "recentQuoteNumber", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="최근견적번호 입력"
+ maxLength={50}
+ />
+ )
+ },
+ size: 130,
+ },
+ {
+ accessorKey: "recentOrderDate",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="최근발주일" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("recentOrderDate")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "recentOrderDate", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="최근발주일 입력 (YYYY-MM-DD)"
+ maxLength={20}
+ autoSave={false}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "recentOrderNumber",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="최근발주번호" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("recentOrderNumber")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "recentOrderNumber", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="최근발주번호 입력"
+ maxLength={50}
+ />
+ )
+ },
+ size: 130,
+ },
+ ]
+ },
+ // 업데이트 히스토리 그룹
+ {
+ header: "업데이트 히스토리",
+ columns: [
+ {
+ accessorKey: "registrationDate",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="등재일" />
+ ),
+ size: 120,
+ },
+ {
+ accessorKey: "registrant",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="등재자" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("registrant") as string
+ return <div className="text-sm">{value || ""}</div>
+ },
+ size: 100,
+ },
+ {
+ accessorKey: "lastModifiedDate",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="최종변경일" />
+ ),
+ size: 120,
+ },
+ {
+ accessorKey: "lastModifier",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="최종변경자" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("lastModifier") as string
+ return <div className="text-sm">{value || ""}</div>
+ },
+ size: 120,
+ },
+ ]
+ },
+ // 액션 그룹
+ {
+ id: "actions",
+ header: "액션",
+ cell: ({ row, table }) => {
+ const data = row.original
+ const isEmptyRow = (table.options.meta as any)?.isEmptyRow?.(String(data.id))
+
+ if (isEmptyRow) {
+ // 빈 행의 경우 저장/취소 버튼 표시
+ return (
+ <div className="flex items-center gap-2 overflow-visible relative">
+ <Button
+ variant="default"
+ size="sm"
+ onClick={() => {
+ const onSaveEmptyRow = (table.options.meta as any)?.onSaveEmptyRow
+ onSaveEmptyRow?.(data.id)
+ }}
+ title="저장"
+ className="bg-green-600 hover:bg-green-700"
+ >
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
+ </svg>
+ </Button>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => {
+ const onCancelEmptyRow = (table.options.meta as any)?.onCancelEmptyRow
+ onCancelEmptyRow?.(data.id)
+ }}
+ title="취소"
+ className="border-red-300 text-red-600 hover:bg-red-50"
+ >
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
+ </svg>
+ </Button>
+ </div>
+ )
+ }
+
+ // 일반 행의 경우 기존 액션 버튼들 표시
+ return (
+ <div className="flex items-center gap-2 overflow-visible relative">
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => {
+ const onAction = (table.options.meta as any)?.onAction
+ onAction?.('delete', data)
+ }}
+ title="삭제"
+ >
+ <Trash2 className="h-4 w-4" />
+ </Button>
+ </div>
+ )
+ },
+ size: 120,
+ enableSorting: false,
+ enableHiding: false,
+ },
+]
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}
+ />
+ </>
+ )
+}