summaryrefslogtreecommitdiff
path: root/lib/avl/table/avl-detail-table.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/avl/table/avl-detail-table.tsx')
-rw-r--r--lib/avl/table/avl-detail-table.tsx455
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>
)
}