diff options
| author | joonhoekim <26rote@gmail.com> | 2025-09-15 18:58:07 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-09-15 18:58:07 +0900 |
| commit | 2b490956c9752c1b756780a3461bc1c37b6fe0a7 (patch) | |
| tree | b0b8a03c8de5dfce4b6c7373a9d608306e9147c0 /lib/avl/table/avl-detail-table.tsx | |
| parent | e7818a457371849e29519497ebf046f385f05ab6 (diff) | |
(김준회) AVL 관리 및 상세 - 기능 구현 1차
+ docker compose 내 오류 수정
Diffstat (limited to 'lib/avl/table/avl-detail-table.tsx')
| -rw-r--r-- | lib/avl/table/avl-detail-table.tsx | 455 |
1 files changed, 46 insertions, 409 deletions
diff --git a/lib/avl/table/avl-detail-table.tsx b/lib/avl/table/avl-detail-table.tsx index ba15c6ef..04384ec8 100644 --- a/lib/avl/table/avl-detail-table.tsx +++ b/lib/avl/table/avl-detail-table.tsx @@ -5,31 +5,13 @@ import * as React from "react" import { useDataTable } from "@/hooks/use-data-table" import { DataTable } from "@/components/data-table/data-table" import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" import { toast } from "sonner" import { columns, type AvlDetailItem } from "./columns-detail" -import { createAvlVendorInfo, updateAvlVendorInfo, deleteAvlVendorInfo, handleAvlAction } from "../service" -import type { AvlDetailItem as AvlDetailType } from "../types" - -// 테이블 메타 타입 확장 -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<AvlDetailItem>> - } -} interface AvlDetailTableProps { data: AvlDetailItem[] pageCount?: number - avlListId: number // 상위 AVL 리스트 ID - onRefresh?: () => void // 데이터 새로고침 콜백 avlType?: '프로젝트AVL' | '선종별표준AVL' // AVL 타입 projectCode?: string // 프로젝트 코드 shipOwnerName?: string // 선주명 @@ -39,386 +21,61 @@ interface AvlDetailTableProps { export function AvlDetailTable({ data, pageCount, - avlListId, - onRefresh, avlType = '프로젝트AVL', projectCode, shipOwnerName, businessType = '조선' }: AvlDetailTableProps) { - // 수정사항 추적 (일괄 저장용) - const [pendingChanges, setPendingChanges] = React.useState<Record<string, Partial<AvlDetailItem>>>({}) - const [isSaving, setIsSaving] = React.useState(false) - - // 빈 행 관리 (신규 등록용) - const [emptyRows, setEmptyRows] = React.useState<Record<string, AvlDetailItem>>({}) - const [isCreating, setIsCreating] = React.useState(false) - - // 검색 상태 - const [searchValue, setSearchValue] = React.useState("") - - - // 인라인 편집 핸들러 (일괄 저장용) - const handleCellUpdate = React.useCallback(async (id: string, field: keyof AvlDetailItem, 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 - } - })) - }, []) - - // 편집 취소 핸들러 - const handleCellCancel = React.useCallback((id: string, field: keyof AvlDetailItem) => { - 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 - } - }) - } - }, []) - // 액션 핸들러 - const handleAction = React.useCallback(async (action: string, data?: any) => { - try { - switch (action) { - case 'new-vendor': - // 신규 협력업체 추가 - 빈 행 추가 - const tempId = `temp-${Date.now()}` - const newEmptyRow: AvlDetailItem = { - id: tempId, - no: 0, - selected: false, - avlListId: avlListId, - equipBulkDivision: "EQUIP", - disciplineCode: "", - disciplineName: "", - materialNameCustomerSide: "", - packageCode: "", - packageName: "", - materialGroupCode: "", - materialGroupName: "", - vendorId: undefined, - vendorName: "", - vendorCode: "", - avlVendorName: "", - tier: "", - faTarget: false, - faStatus: "", - isAgent: false, - agentStatus: "아니오", - contractSignerId: undefined, - contractSignerName: "", - contractSignerCode: "", - headquarterLocation: "", - manufacturingLocation: "", - shiAvl: false, - shiBlacklist: false, - shiBcc: false, - salesQuoteNumber: "", - quoteCode: "", - salesVendorInfo: "", - salesCountry: "", - totalAmount: "", - quoteReceivedDate: "", - recentQuoteDate: "", - recentQuoteNumber: "", - recentOrderDate: "", - recentOrderNumber: "", - remarks: "", - createdAt: new Date().toISOString().split('T')[0], - updatedAt: new Date().toISOString().split('T')[0], - } - - setEmptyRows(prev => ({ - ...prev, - [tempId]: newEmptyRow - })) - toast.success("신규 협력업체 행이 추가되었습니다.") - break - - case 'bulk-import': - // 일괄 입력 - const bulkResult = await handleAvlAction('bulk-import') - if (bulkResult.success) { - toast.success(bulkResult.message) - } else { - toast.error(bulkResult.message) - } - break - - case 'save': - // 변경사항 저장 - if (Object.keys(pendingChanges).length === 0) { - toast.info("저장할 변경사항이 없습니다.") - return - } - - setIsSaving(true) - try { - // 각 변경사항을 순차적으로 저장 - for (const [id, changes] of Object.entries(pendingChanges)) { - if (String(id).startsWith('temp-')) continue // 빈 행은 제외 - - const numericId = Number(id) - if (isNaN(numericId)) { - throw new Error(`유효하지 않은 ID: ${id}`) - } - - const result = await updateAvlVendorInfo(numericId, changes) - if (!result) { - throw new Error(`항목 ${id} 저장 실패`) - } - } - - setPendingChanges({}) - toast.success("변경사항이 저장되었습니다.") - onRefresh?.() - } catch (error) { - console.error('저장 실패:', error) - toast.error("저장 중 오류가 발생했습니다.") - } finally { - setIsSaving(false) - } - break - - case 'edit': - // 수정 모달 열기 (현재는 간단한 토스트로 처리) - toast.info(`${data?.id} 항목 수정`) - break - - case 'delete': - // 삭제 확인 및 실행 - if (!data?.id || String(data.id).startsWith('temp-')) return - - const numericId = Number(data.id) - if (isNaN(numericId)) { - toast.error("유효하지 않은 항목 ID입니다.") - return - } - - const confirmed = window.confirm(`항목 ${data.id}을(를) 삭제하시겠습니까?`) - if (!confirmed) return - - try { - const result = await deleteAvlVendorInfo(numericId) - if (result) { - toast.success("항목이 삭제되었습니다.") - onRefresh?.() - } else { - toast.error("삭제에 실패했습니다.") - } - } catch (error) { - console.error('삭제 실패:', error) - toast.error("삭제 중 오류가 발생했습니다.") - } - break - - case 'avl-form': - // AVL 양식 다운로드/보기 - toast.info("AVL 양식을 준비 중입니다.") - // TODO: AVL 양식 다운로드 로직 구현 - break - - case 'quote-request': - // 견적 요청 - toast.info("견적 요청을 처리 중입니다.") - // TODO: 견적 요청 로직 구현 - break - - case 'vendor-pool': - // Vendor Pool 관리 - toast.info("Vendor Pool을 열고 있습니다.") - // TODO: Vendor Pool 페이지 이동 또는 모달 열기 로직 구현 - break - - case 'download': - // 데이터 다운로드 - toast.info("데이터를 다운로드 중입니다.") - // TODO: 데이터 다운로드 로직 구현 - break - - default: - toast.error(`알 수 없는 액션: ${action}`) - } - } catch (error) { - console.error('액션 처리 실패:', error) - toast.error("액션 처리 중 오류가 발생했습니다.") + const handleAction = React.useCallback(async (action: string) => { + switch (action) { + case 'avl-form': + toast.info("AVL 양식을 준비 중입니다.") + // TODO: AVL 양식 다운로드 로직 구현 + break + + case 'quote-request': + toast.info("견적 요청을 처리 중입니다.") + // TODO: 견적 요청 로직 구현 + break + + case 'vendor-pool': + toast.info("Vendor Pool을 열고 있습니다.") + // TODO: Vendor Pool 페이지 이동 또는 모달 열기 로직 구현 + break + + case 'download': + toast.info("데이터를 다운로드 중입니다.") + // TODO: 데이터 다운로드 로직 구현 + break + + default: + toast.error(`알 수 없는 액션: ${action}`) } - }, [pendingChanges, onRefresh, avlListId]) - - // 빈 행 저장 핸들러 - const handleSaveEmptyRow = React.useCallback(async (tempId: string) => { - const emptyRow = emptyRows[tempId] - if (!emptyRow) return - - try { - setIsCreating(true) - - // 필수 필드 검증 - if (!emptyRow.disciplineName || !emptyRow.vendorName) { - toast.error("설계공종과 협력업체명은 필수 입력 항목입니다.") - return - } - - // 빈 행 데이터를 생성 데이터로 변환 - const createData = { - avlListId: emptyRow.avlListId, - equipBulkDivision: emptyRow.equipBulkDivision, - disciplineCode: emptyRow.disciplineCode || undefined, - disciplineName: emptyRow.disciplineName, - materialNameCustomerSide: emptyRow.materialNameCustomerSide || undefined, - packageCode: emptyRow.packageCode || undefined, - packageName: emptyRow.packageName || undefined, - materialGroupCode: emptyRow.materialGroupCode || undefined, - materialGroupName: emptyRow.materialGroupName || undefined, - vendorId: emptyRow.vendorId, - vendorName: emptyRow.vendorName, - vendorCode: emptyRow.vendorCode || undefined, - avlVendorName: emptyRow.avlVendorName || undefined, - tier: emptyRow.tier || undefined, - faTarget: emptyRow.faTarget ?? false, - faStatus: emptyRow.faStatus || undefined, - isAgent: emptyRow.isAgent ?? false, - contractSignerId: emptyRow.contractSignerId, - contractSignerName: emptyRow.contractSignerName || undefined, - contractSignerCode: emptyRow.contractSignerCode || undefined, - headquarterLocation: emptyRow.headquarterLocation || undefined, - manufacturingLocation: emptyRow.manufacturingLocation || undefined, - hasAvl: emptyRow.shiAvl ?? false, - isBlacklist: emptyRow.shiBlacklist ?? false, - isBcc: emptyRow.shiBcc ?? false, - techQuoteNumber: emptyRow.salesQuoteNumber || undefined, - quoteCode: emptyRow.quoteCode || undefined, - quoteVendorId: emptyRow.vendorId, - quoteVendorName: emptyRow.salesVendorInfo || undefined, - quoteVendorCode: emptyRow.vendorCode, - quoteCountry: emptyRow.salesCountry || undefined, - quoteTotalAmount: emptyRow.totalAmount ? parseFloat(emptyRow.totalAmount.replace(/,/g, '')) : undefined, - quoteReceivedDate: emptyRow.quoteReceivedDate || undefined, - recentQuoteDate: emptyRow.recentQuoteDate || undefined, - recentQuoteNumber: emptyRow.recentQuoteNumber || undefined, - recentOrderDate: emptyRow.recentOrderDate || undefined, - recentOrderNumber: emptyRow.recentOrderNumber || undefined, - remark: emptyRow.remarks || undefined, - } - - const result = await createAvlVendorInfo(createData) - 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(() => { - const emptyRowArray = Object.values(emptyRows) - return [...data, ...emptyRowArray] - }, [data, emptyRows]) - // 테이블 메타 설정 + // 테이블 메타 설정 (읽기 전용) 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]) + }), [handleAction]) // 데이터 테이블 설정 const { table } = useDataTable({ - data: allData, + data, columns, - pageCount, + pageCount: pageCount ?? 1, initialState: { sorting: [{ id: "no", desc: false }], - columnPinning: { right: ["actions"] }, + pagination: { + pageIndex: 0, + pageSize: 10, + }, }, getRowId: (row) => String(row.id), meta: tableMeta, }) - // 변경사항이 있는지 확인 - const hasPendingChanges = Object.keys(pendingChanges).length > 0 - const hasEmptyRows = Object.keys(emptyRows).length > 0 return ( <div className="space-y-4"> @@ -435,45 +92,25 @@ export function AvlDetailTable({ </div> </div> - {/* 상단 버튼 및 검색 영역 */} - <div className="flex items-center justify-between gap-4"> - <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> - <Button variant="outline" size="sm" onClick={() => handleAction('vendor-pool')}> - Vendor Pool - </Button> - <Button variant="outline" size="sm" onClick={() => handleAction('download')}> - 다운로드 - </Button> - </div> - - <div className="flex items-center gap-2"> - <div className="relative"> - <Input - placeholder="검색..." - className="w-64" - value={searchValue} - onChange={(e) => setSearchValue(e.target.value)} - /> - </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> + <Button variant="outline" size="sm" onClick={() => handleAction('vendor-pool')}> + Vendor Pool + </Button> + <Button variant="outline" size="sm" onClick={() => handleAction('download')}> + 다운로드 + </Button> </div> {/* 데이터 테이블 */} <DataTable table={table} /> - {/* 디버그 정보 (개발 환경에서만 표시) */} - {process.env.NODE_ENV === 'development' && (hasPendingChanges || hasEmptyRows) && ( - <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> ) } |
