summaryrefslogtreecommitdiff
path: root/lib/avl/table
diff options
context:
space:
mode:
Diffstat (limited to 'lib/avl/table')
-rw-r--r--lib/avl/table/avl-detail-table.tsx127
-rw-r--r--lib/avl/table/avl-table-columns.tsx213
-rw-r--r--lib/avl/table/avl-table.tsx219
-rw-r--r--lib/avl/table/columns-detail.tsx84
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: "기본 정보",