diff options
| author | joonhoekim <26rote@gmail.com> | 2025-09-15 23:42:46 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-09-15 23:42:46 +0900 |
| commit | 7b0c7c8e56fb027c729c953b0b87dab72156f661 (patch) | |
| tree | c0d968b7157af1e63e3cb083b2872c308b4b3061 /lib/avl/table | |
| parent | 4ee8b24cfadf47452807fa2af801385ed60ab47c (diff) | |
(김준회) 임시 견적요청 및 AVL detail 관련 수정사항 처리
Diffstat (limited to 'lib/avl/table')
| -rw-r--r-- | lib/avl/table/avl-detail-table.tsx | 127 | ||||
| -rw-r--r-- | lib/avl/table/avl-table-columns.tsx | 213 | ||||
| -rw-r--r-- | lib/avl/table/avl-table.tsx | 219 | ||||
| -rw-r--r-- | lib/avl/table/columns-detail.tsx | 84 |
4 files changed, 249 insertions, 394 deletions
diff --git a/lib/avl/table/avl-detail-table.tsx b/lib/avl/table/avl-detail-table.tsx index 04384ec8..4408340a 100644 --- a/lib/avl/table/avl-detail-table.tsx +++ b/lib/avl/table/avl-detail-table.tsx @@ -6,42 +6,92 @@ import { useDataTable } from "@/hooks/use-data-table" import { DataTable } from "@/components/data-table/data-table" import { Button } from "@/components/ui/button" import { toast } from "sonner" +import { createAvlRfqItbAction, prepareAvlRfqItbInput } from "../avl-itb-rfq-service" -import { columns, type AvlDetailItem } from "./columns-detail" +import { columns } from "./columns-detail" +import type { AvlDetailItem } from "../types" +import { BackButton } from "@/components/ui/back-button" +import { useSession } from "next-auth/react" interface AvlDetailTableProps { data: AvlDetailItem[] pageCount?: number avlType?: '프로젝트AVL' | '선종별표준AVL' // AVL 타입 - projectCode?: string // 프로젝트 코드 - shipOwnerName?: string // 선주명 + projectInfo?: { + code?: string + pspid?: string + OWN_NM?: string + kunnrNm?: string + } // 프로젝트 정보 businessType?: string // 사업 유형 (예: 조선/해양) } export function AvlDetailTable({ data, pageCount, - avlType = '프로젝트AVL', - projectCode, - shipOwnerName, - businessType = '조선' + avlType, + projectInfo, + businessType, }: AvlDetailTableProps) { + // 견적요청 처리 상태 관리 + const [isProcessingQuote, setIsProcessingQuote] = React.useState(false) + const { data: session } = useSession() + + // 견적요청 처리 함수 + const handleQuoteRequest = React.useCallback(async () => { + if (!businessType || !['조선', '해양'].includes(businessType)) { + toast.error("공사구분이 올바르지 않습니다. 견적요청 처리 불가.") + return + } + + if (data.length === 0) { + toast.error("견적요청할 AVL 데이터가 없습니다.") + return + } + + setIsProcessingQuote(true) + + try { + // 현재 사용자 세션에서 ID 가져오기 + const currentUserId = session?.user?.id ? Number(session.user.id) : undefined + + // 견적요청 입력 데이터 준비 (전체 데이터를 사용) + const quoteInput = await prepareAvlRfqItbInput( + data, // 전체 데이터를 사용 + businessType as '조선' | '해양', + { + picUserId: currentUserId, + rfqTitle: `${businessType} AVL 견적요청 - ${data[0]?.materialNameCustomerSide || 'AVL 아이템'}${data.length > 1 ? ` 외 ${data.length - 1}건` : ''}` + } + ) + + // 견적요청 실행 + const result = await createAvlRfqItbAction(quoteInput) + + if (result.success) { + toast.success(`${result.data?.type}가 성공적으로 생성되었습니다. (코드: ${result.data?.rfqCode})`) + } else { + toast.error(result.error || "견적요청 처리 중 오류가 발생했습니다.") + } + + } catch (error) { + console.error('견적요청 처리 오류:', error) + toast.error("견적요청 처리 중 오류가 발생했습니다.") + } finally { + setIsProcessingQuote(false) + } + }, [businessType, data, session?.user?.id]) + // 액션 핸들러 const handleAction = React.useCallback(async (action: string) => { switch (action) { - case 'avl-form': - toast.info("AVL 양식을 준비 중입니다.") - // TODO: AVL 양식 다운로드 로직 구현 - break case 'quote-request': - toast.info("견적 요청을 처리 중입니다.") - // TODO: 견적 요청 로직 구현 + await handleQuoteRequest() break case 'vendor-pool': - toast.info("Vendor Pool을 열고 있습니다.") - // TODO: Vendor Pool 페이지 이동 또는 모달 열기 로직 구현 + window.open('/evcp/vendor-pool', '_blank') break case 'download': @@ -49,13 +99,18 @@ export function AvlDetailTable({ // TODO: 데이터 다운로드 로직 구현 break + case 'avl-form': + toast.info("AVL 양식을 준비 중입니다.") + // TODO: AVL 양식 다운로드 로직 구현 + break + default: toast.error(`알 수 없는 액션: ${action}`) } - }, []) + }, [handleQuoteRequest]) - // 테이블 메타 설정 (읽기 전용) + // 테이블 메타 설정 const tableMeta = React.useMemo(() => ({ onAction: handleAction, }), [handleAction]) @@ -76,36 +131,46 @@ export function AvlDetailTable({ meta: tableMeta, }) - return ( <div className="space-y-4"> {/* 상단 정보 표시 영역 */} - <div className="flex items-center justify-between p-4 bg-muted/50 rounded-lg"> + <div className="flex items-center justify-between p-4"> <div className="flex items-center gap-4"> <h2 className="text-lg font-semibold">AVL 상세내역</h2> - <span className="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium"> + <span className="px-3 py-1 bg-secondary-foreground text-secondary-foreground-foreground rounded-full text-sm font-medium"> {avlType} </span> + <span className="text-sm text-muted-foreground"> - [{businessType}] {projectCode || '프로젝트코드'} ({shipOwnerName || '선주명'}) + [{businessType}] {projectInfo?.code || projectInfo?.pspid || '코드정보없음(표준AVL)'} ({projectInfo?.OWN_NM || projectInfo?.kunnrNm || '선주정보 없음'}) </span> </div> + + <div className="justify-end"> + <BackButton>목록으로</BackButton> + </div> </div> {/* 상단 버튼 영역 */} - <div className="flex items-center gap-2"> - <Button variant="outline" size="sm" onClick={() => handleAction('avl-form')}> - AVL양식 - </Button> - <Button variant="outline" size="sm" onClick={() => handleAction('quote-request')}> - 견적요청 - </Button> + <div className="flex items-center gap-2 ml-auto justify-end"> + { + // 표준AVL로는 견적요청하지 않으며, 프로젝트 AVL로만 견적요청처리 + avlType === '프로젝트AVL' && businessType && ['조선', '해양'].includes(businessType) && + <Button + variant="outline" + size="sm" + onClick={() => handleAction('quote-request')} + disabled={data.length === 0 || isProcessingQuote} + > + {isProcessingQuote ? '처리중...' : `${businessType} 견적요청 (${businessType === '조선' ? 'RFQ' : 'ITB'})`} + </Button> + } + + {/* 단순 이동 버튼 */} <Button variant="outline" size="sm" onClick={() => handleAction('vendor-pool')}> Vendor Pool </Button> - <Button variant="outline" size="sm" onClick={() => handleAction('download')}> - 다운로드 - </Button> + </div> {/* 데이터 테이블 */} diff --git a/lib/avl/table/avl-table-columns.tsx b/lib/avl/table/avl-table-columns.tsx index 8caf012e..72c59aa9 100644 --- a/lib/avl/table/avl-table-columns.tsx +++ b/lib/avl/table/avl-table-columns.tsx @@ -2,9 +2,8 @@ import { Checkbox } from "@/components/ui/checkbox" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Eye, Edit, Trash2, History } from "lucide-react" -import { type ColumnDef, TableMeta } from "@tanstack/react-table" +import { type ColumnDef } from "@tanstack/react-table" import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" -import { EditableCell } from "@/components/data-table/editable-cell" import { AvlListItem } from "../types" interface GetColumnsProps { @@ -12,18 +11,12 @@ interface GetColumnsProps { onRowSelect?: (id: number, selected: boolean) => void } -// 수정 여부 확인 헬퍼 함수 -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> + onCellUpdate?: (id: string, field: keyof TData, newValue: string | boolean) => Promise<void> onCellCancel?: (id: string, field: keyof TData) => void - onAction?: (action: string, data?: any) => void + onAction?: (action: string, data?: Partial<AvlListItem>) => void onSaveEmptyRow?: (tempId: string) => Promise<void> onCancelEmptyRow?: (tempId: string) => void isEmptyRow?: (id: string) => boolean @@ -35,7 +28,7 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps): const columns: ColumnDef<AvlListItem>[] = [ // 기본 정보 그룹 { - header: "기본 정보", + header: "AVL 정보", columns: [ { id: "select", @@ -69,25 +62,12 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps): header: ({ column }) => ( <DataTableColumnHeaderSimple column={column} title="AVL 분류" /> ), - cell: ({ getValue, row, table }) => { + cell: ({ getValue }) => { const value = getValue() as boolean - const isModified = getIsModified(table, row.id, "isTemplate") return ( - <EditableCell - value={value ? "표준 AVL" : "프로젝트 AVL"} - isModified={isModified} - type="select" - options={[ - { value: false, label: "프로젝트 AVL" }, - { value: true, label: "표준 AVL" }, - ]} - onUpdate={(newValue) => { - table.options.meta?.onCellUpdate?.(row.id, "isTemplate", newValue === "true") - }} - onCancel={() => { - table.options.meta?.onCellCancel?.(row.id, "isTemplate") - }} - /> + <div className="text-center"> + {value ? "표준 AVL" : "프로젝트 AVL"} + </div> ) }, size: 120, @@ -97,25 +77,12 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps): header: ({ column }) => ( <DataTableColumnHeaderSimple column={column} title="공사부문" /> ), - cell: ({ getValue, row, table }) => { + cell: ({ getValue }) => { const value = getValue() as string - const isModified = getIsModified(table, row.id, "constructionSector") return ( - <EditableCell - value={value} - isModified={isModified} - type="select" - options={[ - { value: "조선", label: "조선" }, - { value: "해양", label: "해양" }, - ]} - onUpdate={(newValue) => { - table.options.meta?.onCellUpdate?.(row.id, "constructionSector", newValue) - }} - onCancel={() => { - table.options.meta?.onCellCancel?.(row.id, "constructionSector") - }} - /> + <div className="text-center"> + {value} + </div> ) }, size: 100, @@ -125,20 +92,12 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps): header: ({ column }) => ( <DataTableColumnHeaderSimple column={column} title="프로젝트 코드" /> ), - cell: ({ getValue, row, table }) => { + cell: ({ getValue }) => { const value = getValue() as string - const isModified = getIsModified(table, row.id, "projectCode") return ( - <EditableCell - value={value} - isModified={isModified} - onUpdate={(newValue) => { - table.options.meta?.onCellUpdate?.(row.id, "projectCode", newValue) - }} - onCancel={() => { - table.options.meta?.onCellCancel?.(row.id, "projectCode") - }} - /> + <div className="text-center"> + {value} + </div> ) }, size: 140, @@ -148,20 +107,12 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps): header: ({ column }) => ( <DataTableColumnHeaderSimple column={column} title="선종" /> ), - cell: ({ getValue, row, table }) => { + cell: ({ getValue }) => { const value = getValue() as string - const isModified = getIsModified(table, row.id, "shipType") return ( - <EditableCell - value={value} - isModified={isModified} - onUpdate={(newValue) => { - table.options.meta?.onCellUpdate?.(row.id, "shipType", newValue) - }} - onCancel={() => { - table.options.meta?.onCellCancel?.(row.id, "shipType") - }} - /> + <div className="text-center"> + {value} + </div> ) }, size: 100, @@ -171,20 +122,12 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps): header: ({ column }) => ( <DataTableColumnHeaderSimple column={column} title="AVL 종류" /> ), - cell: ({ getValue, row, table }) => { + cell: ({ getValue }) => { const value = getValue() as string - const isModified = getIsModified(table, row.id, "avlKind") return ( - <EditableCell - value={value} - isModified={isModified} - onUpdate={(newValue) => { - table.options.meta?.onCellUpdate?.(row.id, "avlKind", newValue) - }} - onCancel={() => { - table.options.meta?.onCellCancel?.(row.id, "avlKind") - }} - /> + <div className="text-center"> + {value} + </div> ) }, size: 120, @@ -194,25 +137,12 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps): header: ({ column }) => ( <DataTableColumnHeaderSimple column={column} title="H/T 구분" /> ), - cell: ({ getValue, row, table }) => { + cell: ({ getValue }) => { const value = getValue() as string - const isModified = getIsModified(table, row.id, "htDivision") return ( - <EditableCell - value={value} - isModified={isModified} - type="select" - options={[ - { value: "H", label: "H" }, - { value: "T", label: "T" }, - ]} - onUpdate={(newValue) => { - table.options.meta?.onCellUpdate?.(row.id, "htDivision", newValue) - }} - onCancel={() => { - table.options.meta?.onCellCancel?.(row.id, "htDivision") - }} - /> + <div className="text-center"> + {value} + </div> ) }, size: 80, @@ -246,9 +176,52 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps): ], }, + // 집계 그룹 + { + header: "등록정보", + columns: [ + { + accessorKey: "PKG", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="PKG" /> + ), + }, + { + accessorKey: "materialGroup", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="자재그룹" /> + ), + }, + { + accessorKey: "vendor", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="협력업체" /> + ), + }, + { + accessorKey: "Tier", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Tier" /> + ), + }, + { + accessorKey: "ownerSuggestion", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="선주 제안" /> + ), + }, + { + accessorKey: "shiSuggestion", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="SHI 제안" /> + ), + }, + ], + }, + // 등록 정보 그룹 { - header: "등록 정보", + header: "작성정보", columns: [ { accessorKey: "createdAt", @@ -262,9 +235,31 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps): size: 100, }, { + accessorKey: "createdBy", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="등재자" /> + ), + cell: ({ getValue }) => { + const date = getValue() as string + return <div className="text-center text-sm">{date}</div> + }, + size: 100, + }, + { accessorKey: "updatedAt", header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="수정일" /> + <DataTableColumnHeaderSimple column={column} title="최종변경일" /> + ), + cell: ({ getValue }) => { + const date = getValue() as string + return <div className="text-center text-sm">{date}</div> + }, + size: 100, + }, + { + accessorKey: "updatedBy", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="최종변경자" /> ), cell: ({ getValue }) => { const date = getValue() as string @@ -320,24 +315,6 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps): > <Eye className="h-4 w-4" /> </Button> - <Button - variant="ghost" - size="sm" - onClick={() => table.options.meta?.onAction?.("edit", { id: row.original.id })} - className="h-8 w-8 p-0" - title="수정" - > - <Edit className="h-4 w-4" /> - </Button> - <Button - variant="ghost" - size="sm" - onClick={() => table.options.meta?.onAction?.("delete", { id: row.original.id })} - className="h-8 w-8 p-0 text-destructive hover:text-destructive" - title="삭제" - > - <Trash2 className="h-4 w-4" /> - </Button> </div> ) }, diff --git a/lib/avl/table/avl-table.tsx b/lib/avl/table/avl-table.tsx index eb9b2079..45da6268 100644 --- a/lib/avl/table/avl-table.tsx +++ b/lib/avl/table/avl-table.tsx @@ -2,7 +2,7 @@ import * as React from "react" import type { - DataTableFilterField, + DataTableAdvancedFilterField } from "@/types/table" import { useDataTable } from "@/hooks/use-data-table" @@ -12,16 +12,17 @@ import { Button } from "@/components/ui/button" import { toast } from "sonner" import { getColumns } from "./avl-table-columns" -import { createAvlListAction, updateAvlListAction, deleteAvlListAction, handleAvlActionAction } from "../service" +import { updateAvlListAction, deleteAvlListAction, handleAvlActionAction } from "../service" import type { AvlListItem } from "../types" import { AvlHistoryModal, type AvlHistoryRecord } from "@/lib/avl/components/avl-history-modal" +import { getAvlHistory } from "@/lib/avl/history-service" // 테이블 메타 타입 확장 declare module "@tanstack/react-table" { interface TableMeta<TData> { - onCellUpdate?: (id: string, field: keyof TData, newValue: any) => Promise<void> + onCellUpdate?: (id: string, field: keyof TData, newValue: string | boolean) => Promise<void> onCellCancel?: (id: string, field: keyof TData) => void - onAction?: (action: string, data?: any) => void + onAction?: (action: string, data?: Partial<AvlListItem>) => void onSaveEmptyRow?: (tempId: string) => Promise<void> onCancelEmptyRow?: (tempId: string) => void isEmptyRow?: (id: string) => boolean @@ -47,10 +48,6 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration const [pendingChanges, setPendingChanges] = React.useState<Record<string, Partial<AvlListItem>>>({}) const [isSaving, setIsSaving] = React.useState(false) - // 빈 행 관리 (신규 등록용) - const [emptyRows, setEmptyRows] = React.useState<Record<string, AvlListItem>>({}) - const [isCreating, setIsCreating] = React.useState(false) - // 히스토리 모달 관리 const [historyModalOpen, setHistoryModalOpen] = React.useState(false) const [selectedAvlItem, setSelectedAvlItem] = React.useState<AvlListItem | null>(null) @@ -58,36 +55,11 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration // 히스토리 데이터 로드 함수 const loadHistoryData = React.useCallback(async (avlItem: AvlListItem): Promise<AvlHistoryRecord[]> => { try { - // 현재 리비전의 스냅샷 데이터 (실제 저장된 데이터 사용) - const currentSnapshot = avlItem.vendorInfoSnapshot || [] - - const historyData: AvlHistoryRecord[] = [ - { - id: avlItem.id, - rev: avlItem.rev || 1, - createdAt: avlItem.createdAt || new Date().toISOString(), - createdBy: avlItem.createdBy || "system", - vendorInfoSnapshot: currentSnapshot, - changeDescription: "최신 리비전 (확정완료)" - } - ] - - // TODO: 실제 구현에서는 DB에서 이전 리비전들의 스냅샷 데이터를 조회해야 함 - // 현재는 더미 데이터로 이전 리비전들을 시뮬레이션 - if ((avlItem.rev || 1) > 1) { - for (let rev = (avlItem.rev || 1) - 1; rev >= 1; rev--) { - historyData.push({ - id: avlItem.id + rev * 1000, // 임시 ID - rev, - createdAt: new Date(Date.now() - rev * 24 * 60 * 60 * 1000).toISOString(), - createdBy: "system", - vendorInfoSnapshot: [], // 이전 리비전의 스냅샷 데이터 (실제로는 DB에서 조회) - changeDescription: `리비전 ${rev} 변경사항` - }) - } - } - - return historyData + const historyRecords = await getAvlHistory(avlItem) + return historyRecords.map(record => ({ + ...record, + vendorInfoSnapshot: record.vendorInfoSnapshot || [] + })) } catch (error) { console.error('히스토리 로드 실패:', error) toast.error("히스토리를 불러오는데 실패했습니다.") @@ -96,7 +68,7 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration }, []) // 필터 필드 정의 - const filterFields: DataTableFilterField<AvlListItem>[] = [ + const filterFields: DataTableAdvancedFilterField<AvlListItem>[] = [ { id: "isTemplate", label: "AVL 분류", @@ -105,6 +77,7 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration { label: "프로젝트 AVL", value: "false" }, { label: "표준 AVL", value: "true" }, ], + type: "select" }, { id: "constructionSector", @@ -114,6 +87,7 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration { label: "조선", value: "조선" }, { label: "해양", value: "해양" }, ], + type: "select" }, { id: "htDivision", @@ -123,17 +97,18 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration { label: "H", value: "H" }, { label: "T", value: "T" }, ], + type: "select" }, ] // 인라인 편집 핸들러 (일괄 저장용) - const handleCellUpdate = React.useCallback(async (id: string, field: keyof AvlListItem, newValue: any) => { + const handleCellUpdate = React.useCallback(async (id: string, field: keyof AvlListItem, newValue: string | boolean) => { const isEmptyRow = String(id).startsWith('temp-') if (isEmptyRow) { - // 빈 행의 경우 emptyRows 상태도 업데이트 - setEmptyRows(prev => ({ + // 빈 행의 경우 pendingChanges 상태도 업데이트 + setPendingChanges(prev => ({ ...prev, [id]: { ...prev[id], @@ -157,15 +132,7 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration const isEmptyRow = String(id).startsWith('temp-') if (isEmptyRow) { - // 빈 행의 경우 emptyRows와 pendingChanges 모두 취소 - setEmptyRows(prev => ({ - ...prev, - [id]: { - ...prev[id], - [field]: prev[id][field] // 원래 값으로 복원 (pendingChanges의 초기값 사용) - } - })) - + // 빈 행의 경우 pendingChanges 취소 setPendingChanges(prev => { const itemChanges = { ...prev[id] } delete itemChanges[field] @@ -185,39 +152,9 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration }, []) // 액션 핸들러 - const handleAction = React.useCallback(async (action: string, data?: any) => { + const handleAction = React.useCallback(async (action: string, data?: Partial<AvlListItem>) => { try { switch (action) { - case 'new-registration': - // 신규 등록 - 빈 행 추가 - const tempId = `temp-${Date.now()}` - const newEmptyRow: AvlListItem = { - id: tempId as any, - no: 0, - selected: false, - isTemplate: false, - constructionSector: "", - projectCode: "", - shipType: "", - avlKind: "", - htDivision: "", - rev: 1, - vendorInfoSnapshot: null, - createdAt: new Date().toISOString().split('T')[0], - updatedAt: new Date().toISOString().split('T')[0], - createdBy: "system", - updatedBy: "system", - registrant: "system", - lastModifier: "system", - } - - setEmptyRows(prev => ({ - ...prev, - [tempId]: newEmptyRow - })) - toast.success("신규 등록 행이 추가되었습니다.") - break - case 'standard-registration': // 표준 AVL 등록 const result = await handleAvlActionAction('standard-registration') @@ -263,7 +200,13 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration for (const [id, changes] of Object.entries(pendingChanges)) { if (String(id).startsWith('temp-')) continue // 빈 행은 제외 - const result = await updateAvlListAction(Number(id), changes as any) + // id 속성을 명시적으로 추가 + const updateData = { + ...changes, + id: Number(id) + } + + const result = await updateAvlListAction(Number(id), updateData) if (!result) { throw new Error(`항목 ${id} 저장 실패`) } @@ -330,95 +273,20 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration } }, [pendingChanges, onRefresh, onRegistrationModeChange]) - // 빈 행 저장 핸들러 - const handleSaveEmptyRow = React.useCallback(async (tempId: string) => { - const emptyRow = emptyRows[tempId] - if (!emptyRow) return - - try { - setIsCreating(true) - - // 필수 필드 검증 - if (!emptyRow.constructionSector || !emptyRow.avlKind) { - toast.error("공사부문과 AVL 종류는 필수 입력 항목입니다.") - return - } - - // 빈 행 데이터를 생성 데이터로 변환 - const createData = { - isTemplate: emptyRow.isTemplate, - constructionSector: emptyRow.constructionSector, - projectCode: emptyRow.projectCode || undefined, - shipType: emptyRow.shipType || undefined, - avlKind: emptyRow.avlKind, - htDivision: emptyRow.htDivision || undefined, - rev: emptyRow.rev, - createdBy: "system", - updatedBy: "system", - } - - const result = await createAvlListAction(createData as any) - if (result) { - // 빈 행 제거 및 성공 메시지 - setEmptyRows(prev => { - const newRows = { ...prev } - delete newRows[tempId] - return newRows - }) - - // pendingChanges에서도 제거 - setPendingChanges(prev => { - const newChanges = { ...prev } - delete newChanges[tempId] - return newChanges - }) - - toast.success("새 항목이 등록되었습니다.") - onRefresh?.() - } else { - toast.error("등록에 실패했습니다.") - } - } catch (error) { - console.error('빈 행 저장 실패:', error) - toast.error("등록 중 오류가 발생했습니다.") - } finally { - setIsCreating(false) - } - }, [emptyRows, onRefresh]) - - // 빈 행 취소 핸들러 - const handleCancelEmptyRow = React.useCallback((tempId: string) => { - setEmptyRows(prev => { - const newRows = { ...prev } - delete newRows[tempId] - return newRows - }) - - setPendingChanges(prev => { - const newChanges = { ...prev } - delete newChanges[tempId] - return newChanges - }) - - toast.info("등록이 취소되었습니다.") - }, []) - // 빈 행 포함한 전체 데이터 const allData = React.useMemo(() => { // 로딩 중에는 빈 데이터를 표시 if (isLoading) { return [] } - const emptyRowArray = Object.values(emptyRows) - return [...data, ...emptyRowArray] - }, [data, emptyRows, isLoading]) + return [...data] + }, [data, isLoading]) // 행 선택 처리 (1개만 선택 가능 - shi-vendor-po 방식) const handleRowSelect = React.useCallback((id: number, selected: boolean) => { if (selected) { setSelectedRows([id]) // 1개만 선택 // 선택된 레코드 찾아서 부모 콜백 호출 - const allData = isLoading ? [] : [...data, ...Object.values(emptyRows)] const selectedRow = allData.find(row => row.id === id) if (selectedRow) { onRowSelect?.(selectedRow) @@ -427,18 +295,16 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration setSelectedRows([]) onRowSelect?.(null) } - }, [data, emptyRows, isLoading, onRowSelect]) + }, [allData, onRowSelect]) // 테이블 메타 설정 const tableMeta = React.useMemo(() => ({ onCellUpdate: handleCellUpdate, onCellCancel: handleCellCancel, onAction: handleAction, - onSaveEmptyRow: handleSaveEmptyRow, - onCancelEmptyRow: handleCancelEmptyRow, isEmptyRow: (id: string) => String(id).startsWith('temp-'), getPendingChanges: () => pendingChanges, - }), [handleCellUpdate, handleCellCancel, handleAction, handleSaveEmptyRow, handleCancelEmptyRow, pendingChanges]) + }), [handleCellUpdate, handleCellCancel, handleAction, pendingChanges]) // 데이터 테이블 설정 @@ -461,29 +327,19 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration // 변경사항이 있는지 확인 const hasPendingChanges = Object.keys(pendingChanges).length > 0 - const hasEmptyRows = Object.keys(emptyRows).length > 0 return ( <div className="space-y-4"> {/* 툴바 */} <DataTableAdvancedToolbar table={table} - filterFields={filterFields as any} + filterFields={filterFields} > <div className="flex items-center gap-2"> {/* 액션 버튼들 */} <Button variant="outline" size="sm" - onClick={() => handleAction('new-registration')} - disabled={isCreating} - > - 신규등록 - </Button> - - <Button - variant="outline" - size="sm" onClick={() => handleAction('standard-registration')} > 표준AVL등록 @@ -497,16 +353,8 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration 프로젝트AVL등록 </Button> - <Button - variant="outline" - size="sm" - onClick={() => handleAction('bulk-import')} - > - 파일 업로드 - </Button> - {/* 저장 버튼 - 변경사항이 있을 때만 활성화 */} - {(hasPendingChanges || hasEmptyRows) && ( + {(hasPendingChanges) && ( <Button variant="default" size="sm" @@ -543,10 +391,9 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration /> {/* 디버그 정보 (개발 환경에서만 표시) */} - {process.env.NODE_ENV === 'development' && (hasPendingChanges || hasEmptyRows) && ( + {process.env.NODE_ENV === 'development' && (hasPendingChanges) && ( <div className="text-xs text-muted-foreground p-2 bg-muted rounded"> <div>Pending Changes: {Object.keys(pendingChanges).length}</div> - <div>Empty Rows: {Object.keys(emptyRows).length}</div> </div> )} </div> diff --git a/lib/avl/table/columns-detail.tsx b/lib/avl/table/columns-detail.tsx index 84ad9d9a..f33ef593 100644 --- a/lib/avl/table/columns-detail.tsx +++ b/lib/avl/table/columns-detail.tsx @@ -2,69 +2,35 @@ import { Checkbox } from "@/components/ui/checkbox" import { Badge } from "@/components/ui/badge" import { type ColumnDef } from "@tanstack/react-table" import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import type { AvlDetailItem } from "../types" -// 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>[] = [ + // 선택 컬럼 + // { + // id: "select", + // header: ({ table }) => ( + // <Checkbox + // checked={ + // table.getIsAllPageRowsSelected() || + // (table.getIsSomePageRowsSelected() && "indeterminate") + // } + // onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + // aria-label="모든 행 선택" + // /> + // ), + // cell: ({ row }) => ( + // <Checkbox + // checked={row.getIsSelected()} + // onCheckedChange={(value) => row.toggleSelected(!!value)} + // aria-label="행 선택" + // /> + // ), + // size: 50, + // enableSorting: false, + // enableHiding: false, + // }, // 기본 정보 그룹 { header: "기본 정보", |
