summaryrefslogtreecommitdiff
path: root/lib/avl/table/columns-detail.tsx
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-09-15 01:23:00 +0000
committerjoonhoekim <26rote@gmail.com>2025-09-15 01:23:00 +0000
commite7818a457371849e29519497ebf046f385f05ab6 (patch)
tree9bf08ba1b31a512c481dc521c9dd7c90091a75b8 /lib/avl/table/columns-detail.tsx
parent3f293c90beb58ce206a66ff444d7acfc41b56429 (diff)
(김준회) AVL 기능 구현 1차 및 벤더풀 E/B 구분 개선
Diffstat (limited to 'lib/avl/table/columns-detail.tsx')
-rw-r--r--lib/avl/table/columns-detail.tsx680
1 files changed, 680 insertions, 0 deletions
diff --git a/lib/avl/table/columns-detail.tsx b/lib/avl/table/columns-detail.tsx
new file mode 100644
index 00000000..204d34f5
--- /dev/null
+++ b/lib/avl/table/columns-detail.tsx
@@ -0,0 +1,680 @@
+import { Checkbox } from "@/components/ui/checkbox"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import { 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
+ }
+}
+
+// AVL 상세 아이템 타입
+export type AvlDetailItem = {
+ id: string
+ no: number
+ selected: boolean
+ // AVL 리스트 ID (외래키)
+ avlListId: number
+ // 설계 정보
+ equipBulkDivision: 'EQUIP' | 'BULK'
+ disciplineCode: string
+ disciplineName: string
+ // 자재 정보
+ materialNameCustomerSide: string
+ packageCode: string
+ packageName: string
+ materialGroupCode: string
+ materialGroupName: string
+ // 협력업체 정보
+ vendorId?: number
+ vendorName: string
+ vendorCode: string
+ avlVendorName: string
+ tier: string
+ // FA 정보
+ faTarget: boolean
+ faStatus: string
+ // Agent 정보
+ isAgent: boolean
+ agentStatus: string // UI 표시용
+ // 계약 서명주체
+ contractSignerId?: number
+ contractSignerName: string
+ contractSignerCode: string
+ // 위치 정보
+ headquarterLocation: string
+ manufacturingLocation: string
+ // SHI Qualification
+ shiAvl: boolean
+ shiBlacklist: boolean
+ shiBcc: boolean
+ // 기술영업 견적결과
+ salesQuoteNumber: string
+ quoteCode: string
+ salesVendorInfo: string
+ salesCountry: string
+ totalAmount: string
+ quoteReceivedDate: string
+ // 업체 실적 현황(구매)
+ recentQuoteDate: string
+ recentQuoteNumber: string
+ recentOrderDate: string
+ recentOrderNumber: string
+ // 기타
+ remarks: string
+ // 타임스탬프
+ createdAt: string
+ updatedAt: string
+}
+
+// 테이블 컬럼 정의
+export const columns: ColumnDef<AvlDetailItem>[] = [
+ // 기본 정보 그룹
+ {
+ 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: 50,
+ },
+ {
+ accessorKey: "no",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="No." />
+ ),
+ size: 60,
+ },
+ {
+ accessorKey: "equipBulkDivision",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="Equip/Bulk 구분" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("equipBulkDivision") as string
+ 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, "equipBulkDivision", newValue)
+ }
+ }
+
+ const isModified = getIsModified(table, row.original.id, "equipBulkDivision")
+
+ return (
+ <EditableCell
+ value={value}
+ type="select"
+ onSave={onSave}
+ options={[
+ { label: "EQUIP", value: "EQUIP" },
+ { label: "BULK", value: "BULK" }
+ ]}
+ placeholder="구분 선택"
+ autoSave={true}
+ disabled={false}
+ initialEditMode={isEmptyRow}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "disciplineName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="설계공종" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("disciplineName")
+ 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, "disciplineName", newValue)
+ }
+ }
+
+ const isModified = getIsModified(table, row.original.id, "disciplineName")
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="설계공종 입력"
+ maxLength={50}
+ autoSave={true}
+ disabled={false}
+ initialEditMode={isEmptyRow}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "materialNameCustomerSide",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="고객사 AVL 자재명" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("materialNameCustomerSide")
+ 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, "materialNameCustomerSide", newValue)
+ }
+ }
+
+ const isModified = getIsModified(table, row.original.id, "materialNameCustomerSide")
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="자재명 입력"
+ maxLength={100}
+ autoSave={true}
+ disabled={false}
+ initialEditMode={isEmptyRow}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 150,
+ },
+ {
+ 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={true}
+ disabled={false}
+ initialEditMode={isEmptyRow}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 130,
+ },
+ {
+ accessorKey: "materialGroupCode",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="자재그룹코드" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("materialGroupCode")
+ 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, "materialGroupCode", newValue)
+ }
+ }
+
+ const isModified = getIsModified(table, row.original.id, "materialGroupCode")
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="자재그룹코드 입력"
+ maxLength={50}
+ autoSave={true}
+ disabled={false}
+ initialEditMode={isEmptyRow}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "materialGroupName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="자재그룹명" />
+ ),
+ 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={true}
+ disabled={false}
+ initialEditMode={isEmptyRow}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 130,
+ },
+ {
+ accessorKey: "vendorCode",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="협력업체코드" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("vendorCode")
+ 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, "vendorCode", newValue)
+ }
+ }
+
+ const isModified = getIsModified(table, row.original.id, "vendorCode")
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="협력업체코드 입력"
+ maxLength={50}
+ autoSave={true}
+ disabled={false}
+ initialEditMode={isEmptyRow}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "vendorName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="협력업체명" />
+ ),
+ 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={true}
+ disabled={false}
+ initialEditMode={isEmptyRow}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 140,
+ },
+ {
+ accessorKey: "avlVendorName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="AVL 등재업체명" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("avlVendorName")
+ 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, "avlVendorName", newValue)
+ }
+ }
+
+ const isModified = getIsModified(table, row.original.id, "avlVendorName")
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="AVL 등재업체명 입력"
+ maxLength={100}
+ autoSave={true}
+ disabled={false}
+ initialEditMode={isEmptyRow}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 140,
+ },
+ {
+ accessorKey: "tier",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="등급 (Tier)" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("tier") as string
+ 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, "tier", newValue)
+ }
+ }
+
+ const isModified = getIsModified(table, row.original.id, "tier")
+
+ 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" }
+ ]}
+ placeholder="등급 선택"
+ autoSave={true}
+ disabled={false}
+ initialEditMode={isEmptyRow}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 100,
+ },
+ ],
+ },
+ // FA 정보 그룹
+ {
+ header: "FA 정보",
+ columns: [
+ {
+ accessorKey: "faTarget",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="FA 대상" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("faTarget") as boolean
+ 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, "faTarget", newValue)
+ }
+ }
+
+ const isModified = getIsModified(table, row.original.id, "faTarget")
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={true}
+ disabled={false}
+ initialEditMode={isEmptyRow}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 80,
+ },
+ {
+ accessorKey: "faStatus",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="FA 현황" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("faStatus")
+ 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, "faStatus", newValue)
+ }
+ }
+
+ const isModified = getIsModified(table, row.original.id, "faStatus")
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="FA 현황 입력"
+ maxLength={50}
+ autoSave={true}
+ disabled={false}
+ initialEditMode={isEmptyRow}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 100,
+ },
+ ],
+ },
+ // SHI Qualification 그룹
+ {
+ header: "SHI Qualification",
+ columns: [
+ {
+ accessorKey: "shiAvl",
+ header: "AVL",
+ cell: ({ row, table }) => {
+ const value = row.getValue("shiAvl") as boolean
+ 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, "shiAvl", newValue)
+ }
+ }
+
+ const isModified = getIsModified(table, row.original.id, "shiAvl")
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={true}
+ disabled={false}
+ initialEditMode={isEmptyRow}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 80,
+ },
+ {
+ accessorKey: "shiBlacklist",
+ header: "Blacklist",
+ cell: ({ row, table }) => {
+ const value = row.getValue("shiBlacklist") as boolean
+ 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, "shiBlacklist", newValue)
+ }
+ }
+
+ const isModified = getIsModified(table, row.original.id, "shiBlacklist")
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={true}
+ disabled={false}
+ initialEditMode={isEmptyRow}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 100,
+ },
+ {
+ accessorKey: "shiBcc",
+ header: "BCC",
+ cell: ({ row, table }) => {
+ const value = row.getValue("shiBcc") as boolean
+ 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, "shiBcc", newValue)
+ }
+ }
+
+ const isModified = getIsModified(table, row.original.id, "shiBcc")
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={true}
+ disabled={false}
+ initialEditMode={isEmptyRow}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 80,
+ },
+ ],
+ },
+ // 액션 컬럼
+ {
+ id: "actions",
+ header: "액션",
+ cell: ({ row, table }) => {
+ const isEmptyRow = String(row.original.id).startsWith('temp-')
+
+ return (
+ <div className="flex items-center gap-2">
+ {!isEmptyRow && (
+ <>
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => table.options.meta?.onAction?.('edit', row.original)}
+ className="h-8 w-8 p-0"
+ >
+ <Edit className="h-4 w-4" />
+ </Button>
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => table.options.meta?.onAction?.('delete', row.original)}
+ className="h-8 w-8 p-0 text-destructive hover:text-destructive"
+ >
+ <Trash2 className="h-4 w-4" />
+ </Button>
+ </>
+ )}
+ {isEmptyRow && (
+ <>
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => table.options.meta?.onSaveEmptyRow?.(row.original.id)}
+ className="h-8 w-8 p-0"
+ >
+ 저장
+ </Button>
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => table.options.meta?.onCancelEmptyRow?.(row.original.id)}
+ className="h-8 w-8 p-0"
+ >
+ 취소
+ </Button>
+ </>
+ )}
+ </div>
+ )
+ },
+ size: 100,
+ enableSorting: false,
+ enableHiding: false,
+ },
+]