From bf2db28586569499e44b58999f2e0f33ed4cdeb5 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Wed, 24 Sep 2025 17:36:08 +0900 Subject: (김준회) 구매 요청사항 반영 - vendor-pool 및 avl detail (이진용 프로) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[lng]/evcp/(evcp)/vendor-pool/page.tsx | 123 +- components/common/project/project-service.ts | 10 +- .../place-of-shipping-selector.tsx | 250 +- .../vendor/vendor-selector-dialog-single.tsx | 1 + components/common/vendor/vendor-service.ts | 4 + lib/avl/avl-itb-rfq-service.ts | 289 -- lib/avl/service.ts | 10 +- lib/avl/table/avl-detail-table.tsx | 70 +- lib/avl/table/avl-registration-area.tsx | 10 - lib/avl/table/avl-table-columns.tsx | 16 +- lib/avl/table/avl-table.tsx | 26 +- lib/avl/table/project-avl-table.tsx | 33 +- lib/avl/table/standard-avl-table.tsx | 11 +- lib/avl/table/vendor-pool-table.tsx | 3 +- lib/vendor-pool/excel-utils.ts | 310 ++ lib/vendor-pool/service.ts | 187 +- .../table/vendor-pool-excel-import-button.tsx | 258 ++ .../table/vendor-pool-table-columns.tsx | 2956 ++++++++++---------- lib/vendor-pool/table/vendor-pool-table.tsx | 224 +- 19 files changed, 2779 insertions(+), 2012 deletions(-) delete mode 100644 lib/avl/avl-itb-rfq-service.ts create mode 100644 lib/vendor-pool/excel-utils.ts create mode 100644 lib/vendor-pool/table/vendor-pool-excel-import-button.tsx diff --git a/app/[lng]/evcp/(evcp)/vendor-pool/page.tsx b/app/[lng]/evcp/(evcp)/vendor-pool/page.tsx index 6708e674..7426e069 100644 --- a/app/[lng]/evcp/(evcp)/vendor-pool/page.tsx +++ b/app/[lng]/evcp/(evcp)/vendor-pool/page.tsx @@ -1,3 +1,5 @@ +"use client" + import * as React from "react" import { type SearchParams } from "@/types/table" @@ -9,24 +11,13 @@ import { getVendorPools } from "@/lib/vendor-pool/service" import { vendorPoolSearchParamsCache } from "@/lib/vendor-pool/validations" import { VendorPoolTable } from "@/lib/vendor-pool/table/vendor-pool-table" import { InformationButton } from "@/components/information/information-button" +import { useSearchParams } from "next/navigation" interface VendorPoolPageProps { searchParams: Promise } -export default async function VendorPoolPage(props: VendorPoolPageProps) { - const searchParams = await props.searchParams - const search = vendorPoolSearchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getVendorPools({ - ...search, - filters: validFilters, - }), - ]) - +export default function VendorPoolPage({ searchParams }: VendorPoolPageProps) { return (
@@ -60,20 +51,118 @@ export default async function VendorPoolPage(props: VendorPoolPageProps) { /> } > - + ) } -// 실제 데이터를 받아서 VendorPoolTable에 전달하는 컴포넌트 -function VendorPoolTableWrapper({ promises }: { promises: Promise }) { - const [{ data, pageCount }] = React.use(promises) +// 클라이언트 컴포넌트: 필터 변경을 감시하여 데이터 재조회 +function VendorPoolTableWrapperClient({ searchParamsPromise }: { searchParamsPromise: Promise }) { + const searchParams = useSearchParams() + const [initialData, setInitialData] = React.useState<{ data: any[], pageCount: number } | null>(null) + const [data, setData] = React.useState([]) + const [pageCount, setPageCount] = React.useState(0) + const [isLoading, setIsLoading] = React.useState(false) + + // 초기 데이터 로딩 + React.useEffect(() => { + const loadInitialData = async () => { + try { + const searchParamsData = await searchParamsPromise + const search = vendorPoolSearchParamsCache.parse(searchParamsData) + const validFilters = getValidFilters(search.filters) + + const result = await getVendorPools({ + ...search, + filters: validFilters, + }) + + setInitialData(result) + setData(result.data) + setPageCount(result.pageCount) + } catch (error) { + console.error('Failed to load initial data:', error) + } + } + loadInitialData() + }, [searchParamsPromise]) + + // 필터 상태 변경 감시 및 데이터 재조회 + React.useEffect(() => { + if (!initialData) return // 초기 데이터가 로드되기 전까지는 실행하지 않음 + + const refreshData = async () => { + setIsLoading(true) + try { + const currentParams = Object.fromEntries(searchParams.entries()) + const search = vendorPoolSearchParamsCache.parse(currentParams) + const validFilters = getValidFilters(search.filters) + + const result = await getVendorPools({ + ...search, + filters: validFilters, + }) + + setData(result.data) + setPageCount(result.pageCount) + } catch (error) { + console.error('Failed to refresh vendor pool data:', error) + } finally { + setIsLoading(false) + } + } + + // 필터 파라미터가 변경될 때마다 데이터 재조회 + const currentFilters = searchParams.get('filters') + const currentJoinOperator = searchParams.get('joinOperator') + const currentSearch = searchParams.get('search') + const currentPage = searchParams.get('page') + const currentPerPage = searchParams.get('perPage') + const currentSort = searchParams.get('sort') + + // 필터 관련 파라미터가 변경되면 재조회 + if (currentFilters !== '[]' || currentJoinOperator || currentSearch || currentPage || currentPerPage || currentSort) { + refreshData() + } else { + // 필터가 초기 상태면 초기 데이터로 복원 + setData(initialData.data) + setPageCount(initialData.pageCount) + } + }, [searchParams, initialData]) + + const handleRefresh = React.useCallback(async () => { + if (!initialData) return + + setIsLoading(true) + try { + const currentParams = Object.fromEntries(searchParams.entries()) + const search = vendorPoolSearchParamsCache.parse(currentParams) + const validFilters = getValidFilters(search.filters) + + const result = await getVendorPools({ + ...search, + filters: validFilters, + }) + + setData(result.data) + setPageCount(result.pageCount) + } catch (error) { + console.error('Failed to refresh vendor pool data:', error) + } finally { + setIsLoading(false) + } + }, [searchParams, initialData]) + + if (!initialData) { + return null // 초기 데이터 로딩 중 + } return ( ) } diff --git a/components/common/project/project-service.ts b/components/common/project/project-service.ts index 6c103c6f..510d7527 100644 --- a/components/common/project/project-service.ts +++ b/components/common/project/project-service.ts @@ -28,6 +28,10 @@ export interface ProjectInfo { shipType?: string | null projectMsrm?: string | null projectHtDivision?: string | null + type?: string | null // projects 테이블의 type 필드 + sector?: string | null // biddingProjects 테이블의 sector 필드 + typeMdg?: string | null // projects 테이블의 TYPE_MDG 필드 + pjtType?: string | null // biddingProjects 테이블의 pjtType 필드 source: 'projects' | 'biddingProjects' } @@ -145,13 +149,15 @@ export async function getProjectInfoByCode(projectCode: string, searchFrom: 'bot if (searchFrom === 'both' || searchFrom === 'projects') { try { const projectInfo = await db.select().from(projects).where(eq(projects.code, projectCode)).limit(1) - + if (projectInfo && projectInfo.length > 0) { return { projectCode: projectInfo[0].code, projectName: projectInfo[0].name, shipType: projectInfo[0].SKND || undefined, projectHtDivision: projectInfo[0].type || undefined, + type: projectInfo[0].type || undefined, + typeMdg: projectInfo[0].TYPE_MDG || undefined, source: 'projects' } } @@ -180,6 +186,8 @@ export async function getProjectInfoByCode(projectCode: string, searchFrom: 'bot projectName: projectInfo[0].projNm, projectMsrm: projectInfo[0].ptypeNm, projectHtDivision, + sector: projectInfo[0].sector || undefined, + pjtType: projectInfo[0].pjtType || undefined, source: 'biddingProjects' } } diff --git a/components/common/selectors/place-of-shipping/place-of-shipping-selector.tsx b/components/common/selectors/place-of-shipping/place-of-shipping-selector.tsx index 63532365..0a9916cd 100644 --- a/components/common/selectors/place-of-shipping/place-of-shipping-selector.tsx +++ b/components/common/selectors/place-of-shipping/place-of-shipping-selector.tsx @@ -13,8 +13,27 @@ import { Select, SelectItem, SelectContent } from "@/components/ui/select" import { SelectTrigger } from "@/components/ui/select" import { SelectValue } from "@/components/ui/select" import { Input } from "@/components/ui/input" -import { useState, useEffect, useMemo } from "react" +import { useState, useEffect, useMemo, useCallback } from "react" import { getPlaceOfShippingForSelection } from "./place-of-shipping-service" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Search, Check } from "lucide-react" interface PlaceOfShippingData { code: string @@ -96,4 +115,233 @@ export function PlaceOfShippingSelector({
) +} + +/** + * 선적지/하역지 단일 선택 Dialog 컴포넌트 + * + * @description + * - PlaceOfShippingSelector를 Dialog로 래핑한 단일 선택 컴포넌트 + * - 버튼 클릭 시 Dialog가 열리고, 장소를 선택하면 Dialog가 닫히며 결과를 반환 + * + * @PlaceOfShippingData_Structure + * 선택된 장소 객체의 형태: + * ```typescript + * interface PlaceOfShippingData { + * code: string; // 장소코드 + * description: string; // 장소명 + * } + * ``` + * + * @state + * - open: Dialog 열림/닷힘 상태 + * - selectedPlace: 현재 선택된 장소 (단일) + * - tempSelectedPlace: Dialog 내에서 임시로 선택된 장소 (확인 버튼 클릭 전까지) + * + * @callback + * - onPlaceSelect: 장소 선택 완료 시 호출되는 콜백 + * - 매개변수: PlaceOfShippingData | null + * - 선택된 장소 정보 또는 null (선택 해제 시) + * + * @usage + * ```tsx + * { + * setSelectedPlace(place); + * console.log('선택된 장소:', place); + * }} + * placeholder="장소를 검색하세요..." + * /> + * ``` + */ + +interface PlaceOfShippingSelectorDialogSingleProps { + /** Dialog를 여는 트리거 버튼 텍스트 */ + triggerLabel?: string + /** 현재 선택된 장소 */ + selectedPlace?: PlaceOfShippingData | null + /** 장소 선택 완료 시 호출되는 콜백 */ + onPlaceSelect?: (place: PlaceOfShippingData | null) => void + /** 검색 입력창 placeholder */ + placeholder?: string + /** Dialog 제목 */ + title?: string + /** Dialog 설명 */ + description?: string + /** 트리거 버튼 비활성화 여부 */ + disabled?: boolean + /** 트리거 버튼 variant */ + triggerVariant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" +} + +export function PlaceOfShippingSelectorDialogSingle({ + triggerLabel = "장소 선택", + selectedPlace = null, + onPlaceSelect, + placeholder = "장소를 검색하세요...", + title = "장소 선택", + description = "원하는 장소를 검색하고 선택해주세요.", + disabled = false, + triggerVariant = "outline", +}: PlaceOfShippingSelectorDialogSingleProps) { + // Dialog 열림/닫힘 상태 + const [open, setOpen] = useState(false) + + // Dialog 내에서 임시로 선택된 장소 (확인 버튼 클릭 전까지) + const [tempSelectedPlace, setTempSelectedPlace] = useState(null) + + // 장소 데이터 + const [placeOfShippingData, setPlaceOfShippingData] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [searchTerm, setSearchTerm] = useState("") + + const filteredData = useMemo(() => { + if (!searchTerm) return placeOfShippingData + return placeOfShippingData.filter(item => + item.code.toLowerCase().includes(searchTerm.toLowerCase()) || + item.description.toLowerCase().includes(searchTerm.toLowerCase()) + ) + }, [placeOfShippingData, searchTerm]) + + // Dialog 열림 시 현재 선택된 장소로 임시 선택 초기화 + const handleOpenChange = useCallback((newOpen: boolean) => { + setOpen(newOpen) + if (newOpen) { + setTempSelectedPlace(selectedPlace || null) + } + }, [selectedPlace]) + + // 장소 선택 처리 (Dialog 내에서) + const handlePlaceChange = useCallback((place: PlaceOfShippingData) => { + setTempSelectedPlace(place) + }, []) + + // 확인 버튼 클릭 시 선택 완료 + const handleConfirm = useCallback(() => { + onPlaceSelect?.(tempSelectedPlace) + setOpen(false) + }, [tempSelectedPlace, onPlaceSelect]) + + // 취소 버튼 클릭 시 + const handleCancel = useCallback(() => { + setTempSelectedPlace(selectedPlace || null) + setOpen(false) + }, [selectedPlace]) + + // 선택 해제 + const handleClear = useCallback(() => { + setTempSelectedPlace(null) + }, []) + + useEffect(() => { + const loadData = async () => { + try { + const data = await getPlaceOfShippingForSelection() + setPlaceOfShippingData(data) + } catch (error) { + console.error('선적지/하역지 데이터 로드 실패:', error) + setPlaceOfShippingData([]) + } finally { + setIsLoading(false) + } + } + + loadData() + }, []) + + return ( + + + + + + + + {title} + {description} + + +
+
+ + setSearchTerm(e.target.value)} + className="flex-1" + /> +
+ + {isLoading ? ( +
+
장소 데이터를 불러오는 중...
+
+ ) : ( +
+ + + + + 장소코드 + 장소명 + + + + {filteredData.length === 0 ? ( + + + {searchTerm ? "검색 결과가 없습니다" : "데이터가 없습니다"} + + + ) : ( + filteredData.map((item) => ( + handlePlaceChange(item)} + > + + {tempSelectedPlace?.code === item.code && ( + + )} + + {item.code} + {item.description} + + )) + )} + +
+
+ )} +
+ + + + {tempSelectedPlace && ( + + )} + + +
+
+ ) } \ No newline at end of file diff --git a/components/common/vendor/vendor-selector-dialog-single.tsx b/components/common/vendor/vendor-selector-dialog-single.tsx index da9a9a74..7bb4b14c 100644 --- a/components/common/vendor/vendor-selector-dialog-single.tsx +++ b/components/common/vendor/vendor-selector-dialog-single.tsx @@ -28,6 +28,7 @@ import { VendorSearchItem } from "./vendor-service" * id: number; // 벤더 ID * vendorName: string; // 벤더명 * vendorCode: string | null; // 벤더코드 (없을 수 있음) + * taxId: string | null; // 사업자번호 * status: string; // 벤더 상태 (ACTIVE, PENDING_REVIEW 등) * displayText: string; // 표시용 텍스트 (vendorName + vendorCode) * } diff --git a/components/common/vendor/vendor-service.ts b/components/common/vendor/vendor-service.ts index 83a63cae..1c59843c 100644 --- a/components/common/vendor/vendor-service.ts +++ b/components/common/vendor/vendor-service.ts @@ -9,6 +9,7 @@ export interface VendorSearchItem { id: number vendorName: string vendorCode: string | null + taxId: string | null // 사업자번호 status: string displayText: string // vendorName + vendorCode로 구성된 표시용 텍스트 } @@ -100,6 +101,7 @@ export async function searchVendorsForSelector( id: vendors.id, vendorName: vendors.vendorName, vendorCode: vendors.vendorCode, + taxId: vendors.taxId, status: vendors.status, }) .from(vendors) @@ -169,6 +171,7 @@ export async function getAllVendors(): Promise<{ id: vendors.id, vendorName: vendors.vendorName, vendorCode: vendors.vendorCode, + taxId: vendors.taxId, status: vendors.status, }) .from(vendors) @@ -209,6 +212,7 @@ export async function getVendorById(vendorId: number): Promise { - try { - // 동일한 userCode를 가진 마지막 RFQ/ITB 번호 조회 - const lastRfq = await db - .select({ rfqCode: rfqsLast.rfqCode }) - .from(rfqsLast) - .where( - and( - eq(rfqsLast.picCode, userCode), - type === 'RFQ' - ? sql`${rfqsLast.prNumber} IS NOT NULL AND ${rfqsLast.prNumber} != ''` - : sql`${rfqsLast.projectCompany} IS NOT NULL AND ${rfqsLast.projectCompany} != ''` - ) - ) - .orderBy(desc(rfqsLast.createdAt)) - .limit(1); - - let nextNumber = 1; - - if (lastRfq.length > 0 && lastRfq[0].rfqCode) { - // 마지막 코드에서 숫자 부분 추출 (ex: "RFQ001001" -> "001") - const codeMatch = lastRfq[0].rfqCode.match(/([A-Z]{3})(\d{3})(\d{3})/); - if (codeMatch) { - const currentNumber = parseInt(codeMatch[3]); - nextNumber = currentNumber + 1; - } - } - - // 코드 형식: RFQ/ITB + userCode(3자리) + 일련번호(3자리) - const prefix = type === 'RFQ' ? 'RFQ' : 'ITB'; - const paddedNumber = nextNumber.toString().padStart(3, '0'); - - return `${prefix}${userCode}${paddedNumber}`; - } catch (error) { - console.error('RFQ/ITB 코드 생성 오류:', error); - // 오류 발생 시 기본 코드 생성 - const prefix = type === 'RFQ' ? 'RFQ' : 'ITB'; - return `${prefix}${userCode}001`; - } -} - -// AVL 기반 RFQ/ITB 생성을 위한 입력 타입 -export interface CreateAvlRfqItbInput { - // AVL 정보 - avlItems: AvlDetailItem[] - businessType: '조선' | '해양' // 조선: RFQ, 해양: ITB - - // RFQ/ITB 공통 정보 - rfqTitle: string - dueDate: Date - remark?: string - - // 담당자 정보 - picUserId: number - - // 추가 정보 (ITB용) - projectCompany?: string - projectFlag?: string - projectSite?: string - smCode?: string - - // PR 정보 (RFQ용) - prNumber?: string - prIssueDate?: Date - series?: string -} - -// RFQ/ITB 생성 결과 타입 -export interface CreateAvlRfqItbResult { - success: boolean - message?: string - data?: { - id: number - rfqCode: string - type: 'RFQ' | 'ITB' - } - error?: string -} - -/** - * AVL 기반 RFQ/ITB 생성 서비스 - * - 조선 사업: RFQ 생성 - * - 해양 사업: ITB 생성 - * - rfqLast 테이블에 직접 데이터 삽입 - */ -export async function createAvlRfqItbAction(input: CreateAvlRfqItbInput): Promise { - try { - // 세션 확인 - const session = await getServerSession(authOptions) - if (!session?.user?.id) { - return { - success: false, - error: '로그인이 필요합니다.' - } - } - - // 입력 검증 - if (!input.avlItems || input.avlItems.length === 0) { - return { - success: false, - error: '견적 요청할 AVL 아이템이 없습니다.' - } - } - - if (!input.businessType || !['조선', '해양'].includes(input.businessType)) { - return { - success: false, - error: '올바른 사업 유형을 선택해주세요.' - } - } - - // 담당자 정보 확인 - const picUser = await db - .select({ - id: users.id, - name: users.name, - userCode: users.userCode - }) - .from(users) - .where(eq(users.id, input.picUserId)) - .limit(1) - - if (!picUser || picUser.length === 0) { - return { - success: false, - error: '담당자를 찾을 수 없습니다.' - } - } - - const userCode = picUser[0].userCode; - if (!userCode || userCode.length !== 3) { - return { - success: false, - error: '담당자의 userCode가 올바르지 않습니다 (3자리 필요)' - } - } - - // 사업 유형에 따른 RFQ/ITB 구분 및 데이터 준비 - const rfqType = input.businessType === '조선' ? 'RFQ' : 'ITB' - const rfqTypeLabel = rfqType - - // RFQ/ITB 코드 생성 - const rfqCode = await generateAvlRfqItbCode(userCode, rfqType) - - // 대표 아이템 정보 추출 (첫 번째 아이템) - const representativeItem = input.avlItems[0] - - // 트랜잭션으로 RFQ/ITB 생성 - const result = await db.transaction(async (tx) => { - // 1. rfqsLast 테이블에 기본 정보 삽입 - const [newRfq] = await tx - .insert(rfqsLast) - .values({ - rfqCode, - status: "RFQ 생성", - dueDate: input.dueDate, - - // 대표 아이템 정보 - itemCode: representativeItem.materialGroupCode || `AVL-${representativeItem.id}`, - itemName: representativeItem.materialNameCustomerSide || representativeItem.materialGroupName || 'AVL 아이템', - - // 담당자 정보 - pic: input.picUserId, - picCode: userCode, - picName: picUser[0].name || '', - - // 기타 정보 - remark: input.remark || null, - createdBy: Number(session.user.id), - updatedBy: Number(session.user.id), - createdAt: new Date(), - updatedAt: new Date(), - - // 사업 유형별 추가 필드 - ...(input.businessType === '조선' && { - // RFQ 필드 - prNumber: input.prNumber || rfqCode, // PR 번호가 없으면 RFQ 코드 사용 - prIssueDate: input.prIssueDate || new Date(), - series: input.series || null - }), - - ...(input.businessType === '해양' && { - // ITB 필드 - projectCompany: input.projectCompany || 'AVL 기반 프로젝트', - projectFlag: input.projectFlag || null, - projectSite: input.projectSite || null, - smCode: input.smCode || null - }) - }) - .returning() - - // 2. rfqPrItems 테이블에 AVL 아이템들 삽입 - const prItemsData = input.avlItems.map((item, index) => ({ - rfqsLastId: newRfq.id, - rfqItem: `${index + 1}`.padStart(3, '0'), // 001, 002, ... - prItem: `${index + 1}`.padStart(3, '0'), - prNo: rfqCode, - - materialCode: item.materialGroupCode || `AVL-${item.id}`, - materialDescription: item.materialNameCustomerSide || item.materialGroupName || `AVL 아이템 ${index + 1}`, - materialCategory: item.materialGroupCode || null, - - quantity: 1, // AVL에서는 수량 정보가 없으므로 1로 설정 - uom: 'EA', // 기본 단위 - - majorYn: index === 0, // 첫 번째 아이템을 주요 아이템으로 설정 - - deliveryDate: input.dueDate, // 납기일은 RFQ 마감일과 동일하게 설정 - })) - - await tx.insert(rfqPrItems).values(prItemsData) - - return newRfq - }) - - // 성공 결과 반환 - return { - success: true, - message: `${rfqTypeLabel}가 성공적으로 생성되었습니다.`, - data: { - id: result.id, - rfqCode: result.rfqCode!, - type: rfqTypeLabel as 'RFQ' | 'ITB' - } - } - - } catch (error) { - console.error('AVL RFQ/ITB 생성 오류:', error) - - if (error instanceof Error) { - return { - success: false, - error: error.message - } - } - - return { - success: false, - error: '알 수 없는 오류가 발생했습니다.' - } - } -} - -/** - * AVL 데이터에서 RFQ/ITB 생성을 위한 기본값 설정 헬퍼 함수 - */ -export async function prepareAvlRfqItbInput( - selectedItems: AvlDetailItem[], - businessType: '조선' | '해양', - defaultValues?: Partial -): Promise { - const now = new Date() - const dueDate = new Date(now.getTime() + (30 * 24 * 60 * 60 * 1000)) // 30일 후 - - // 선택된 아이템들의 대표 정보를 추출하여 제목 생성 - const representativeItem = selectedItems[0] - const itemCount = selectedItems.length - const titleSuffix = itemCount > 1 ? ` 외 ${itemCount - 1}건` : '' - const defaultTitle = `${representativeItem?.materialNameCustomerSide || 'AVL 자재'}${titleSuffix}` - - return { - avlItems: selectedItems, - businessType, - rfqTitle: defaultValues?.rfqTitle || `${businessType} - ${defaultTitle}`, - dueDate: defaultValues?.dueDate || dueDate, - remark: defaultValues?.remark || `AVL 기반 ${businessType} 견적 요청`, - picUserId: defaultValues?.picUserId || 0, // 호출 측에서 설정 필요 - // ITB용 필드들 - projectCompany: defaultValues?.projectCompany, - projectFlag: defaultValues?.projectFlag, - projectSite: defaultValues?.projectSite, - smCode: defaultValues?.smCode, - // RFQ용 필드들 - prNumber: defaultValues?.prNumber, - prIssueDate: defaultValues?.prIssueDate, - series: defaultValues?.series - } -} diff --git a/lib/avl/service.ts b/lib/avl/service.ts index 0340f52c..1f781486 100644 --- a/lib/avl/service.ts +++ b/lib/avl/service.ts @@ -438,14 +438,8 @@ export async function handleAvlAction( ): Promise { try { switch (action) { - case "new-registration": - return { success: true, message: "신규 AVL 등록 모드" }; - - case "standard-registration": - return { success: true, message: "표준 AVL 등재 모드" }; - - case "project-registration": - return { success: true, message: "프로젝트 AVL 등재 모드" }; + case "avl-registration": + return { success: true, message: "AVL 등록 패널을 활성화했습니다." }; case "bulk-import": if (!data?.file) { diff --git a/lib/avl/table/avl-detail-table.tsx b/lib/avl/table/avl-detail-table.tsx index 4408340a..22c503ff 100644 --- a/lib/avl/table/avl-detail-table.tsx +++ b/lib/avl/table/avl-detail-table.tsx @@ -6,12 +6,10 @@ 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 } 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[] @@ -33,63 +31,11 @@ export function AvlDetailTable({ 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 'quote-request': - await handleQuoteRequest() - break - case 'vendor-pool': window.open('/evcp/vendor-pool', '_blank') break @@ -107,7 +53,7 @@ export function AvlDetailTable({ default: toast.error(`알 수 없는 액션: ${action}`) } - }, [handleQuoteRequest]) + }, []) // 테이블 메타 설정 @@ -153,24 +99,10 @@ export function AvlDetailTable({ {/* 상단 버튼 영역 */}
- { - // 표준AVL로는 견적요청하지 않으며, 프로젝트 AVL로만 견적요청처리 - avlType === '프로젝트AVL' && businessType && ['조선', '해양'].includes(businessType) && - - } - {/* 단순 이동 버튼 */} -
{/* 데이터 테이블 */} diff --git a/lib/avl/table/avl-registration-area.tsx b/lib/avl/table/avl-registration-area.tsx index ba1c76d4..6c7eba9d 100644 --- a/lib/avl/table/avl-registration-area.tsx +++ b/lib/avl/table/avl-registration-area.tsx @@ -15,16 +15,6 @@ import { toast } from "sonner" // 선택된 테이블 타입 type SelectedTable = 'project' | 'standard' | 'vendor' | null -// TODO: 나머지 테이블들도 ref 지원 추가 시 인터페이스 추가 필요 -// interface StandardAvlTableRef { -// getSelectedIds?: () => number[] -// } -// -// interface VendorPoolTableRef { -// getSelectedIds?: () => number[] -// } - - // 선택 상태 액션 타입 type SelectionAction = | { type: 'SELECT_PROJECT'; count: number } diff --git a/lib/avl/table/avl-table-columns.tsx b/lib/avl/table/avl-table-columns.tsx index 6ec2c3db..06005d3d 100644 --- a/lib/avl/table/avl-table-columns.tsx +++ b/lib/avl/table/avl-table-columns.tsx @@ -45,9 +45,7 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps): enableSorting: false, enableHiding: false, enableResizing: false, - size: 10, - minSize: 10, - maxSize: 10, + size: 50, }, // No 컬럼 { @@ -57,7 +55,7 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps): ), cell: ({ getValue }) =>
{getValue() as number}
, enableResizing: true, - size: 60, + size: 100, }, // AVL 분류 컬럼 { @@ -148,7 +146,7 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps): { accessorKey: "htDivision", header: ({ column }) => ( - + ), cell: ({ getValue }) => { const value = getValue() as string @@ -205,7 +203,7 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps): ), enableResizing: true, - size: 120, + size: 100, }, // 협력업체 컬럼 { @@ -214,7 +212,7 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps): ), enableResizing: true, - size: 150, + size: 100, }, // Tier 컬럼 { @@ -280,7 +278,7 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps): return
{date}
}, enableResizing: true, - size: 100, + size: 120, }, // 최종변경자 컬럼 { @@ -293,7 +291,7 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps): return
{value}
}, enableResizing: true, - size: 100, + size: 120, }, // 액션 컬럼 { diff --git a/lib/avl/table/avl-table.tsx b/lib/avl/table/avl-table.tsx index 61db658d..9b6ac90b 100644 --- a/lib/avl/table/avl-table.tsx +++ b/lib/avl/table/avl-table.tsx @@ -100,9 +100,9 @@ export function AvlTable({ data, pageCount, isLoading, onRegistrationModeChange, const handleAction = React.useCallback(async (action: string, data?: Partial) => { try { switch (action) { - case 'standard-registration': - // 표준 AVL 등록 - const result = await handleAvlActionAction('standard-registration') + case 'avl-registration': + // AVL 등록 (통합된 기능) + const result = await handleAvlActionAction('avl-registration') if (result.success) { toast.success(result.message) onRegistrationModeChange?.('standard') // 등록 모드 변경 콜백 호출 @@ -112,9 +112,10 @@ export function AvlTable({ data, pageCount, isLoading, onRegistrationModeChange, break case 'view-detail': - // 상세 조회 (페이지 이동) + // 상세 조회 (페이지 이동) - 원래 방식으로 복원 if (data?.id && !String(data.id).startsWith('temp-')) { - window.location.href = `/evcp/avl/${data.id}` + console.log('AVL 상세보기 이동:', data.id) // 디버깅용 + window.location.href = `/ko/evcp/avl/${data.id}` } break @@ -177,6 +178,9 @@ export function AvlTable({ data, pageCount, isLoading, onRegistrationModeChange, columnSizing: {}, }, getRowId: (row) => String(row.id), + meta: { + onAction: handleAction, + }, }) return ( @@ -191,17 +195,9 @@ export function AvlTable({ data, pageCount, isLoading, onRegistrationModeChange, - - diff --git a/lib/avl/table/project-avl-table.tsx b/lib/avl/table/project-avl-table.tsx index 9584c6f9..ad72b221 100644 --- a/lib/avl/table/project-avl-table.tsx +++ b/lib/avl/table/project-avl-table.tsx @@ -177,12 +177,40 @@ export const ProjectAvlTable = forwardRef { setSearchConstructionSector(value) - // 공사부문이 변경되면 선종을 빈 값으로 초기화 - setSelectedShipType(undefined) }, []) // 검색 상태 변경 시 부모 컴포넌트에 전달 @@ -506,7 +504,7 @@ export const StandardAvlTable = forwardRef - + @@ -552,7 +549,7 @@ export const StandardAvlTable = forwardRef + + + ) +} diff --git a/lib/vendor-pool/table/vendor-pool-table-columns.tsx b/lib/vendor-pool/table/vendor-pool-table-columns.tsx index 8f09e684..1f0c455e 100644 --- a/lib/vendor-pool/table/vendor-pool-table-columns.tsx +++ b/lib/vendor-pool/table/vendor-pool-table-columns.tsx @@ -1,7 +1,7 @@ import { Checkbox } from "@/components/ui/checkbox" import { Button } from "@/components/ui/button" -import { MoreHorizontal, Eye, Edit, Trash2 } from "lucide-react" -import { type ColumnDef, TableMeta } from "@tanstack/react-table" +import { Trash2 } from "lucide-react" +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" @@ -11,1566 +11,1530 @@ const getIsModified = (table: any, rowId: string, fieldName: string) => { return String(rowId) in pendingChanges && fieldName in pendingChanges[String(rowId)] } -// 테이블 메타 타입 확장 -declare module "@tanstack/react-table" { - interface TableMeta { - onCellUpdate?: (id: string, field: keyof TData, newValue: any) => Promise - onCellCancel?: (id: string, field: keyof TData) => void - onAction?: (action: string, data?: any) => void - onSaveEmptyRow?: (tempId: string) => Promise - onCancelEmptyRow?: (tempId: string) => void - isEmptyRow?: (id: string) => boolean - onTaxIdChange?: (id: string, taxId: string) => Promise - onMaterialGroupCodeChange?: (id: string, materialGroupCode: string) => Promise - } +// vendor-pool 테이블 메타 타입 (복사본) +interface VendorPoolTableMeta { + onCellUpdate?: (id: string | number, field: string, newValue: any) => Promise + onCellCancel?: (id: string | number, field: string) => void + onAction?: (action: string, data?: any) => void + onSaveEmptyRow?: (tempId: string) => Promise + onCancelEmptyRow?: (tempId: string) => void + isEmptyRow?: (id: string) => boolean + onTaxIdChange?: (id: string, taxId: string) => Promise } -// Vendor Pool 데이터 타입 -export type VendorPoolItem = { - id: string - no: number - selected: boolean - constructionSector: string // 공사부문: 조선 또는 해양 - htDivision: string // H/T구분: H 또는 T - designCategoryCode: string // 설계기능(공종) 코드: 2자리 영문대문자 - designCategory: string // 설계기능(공종): 전장 등 - equipBulkDivision: string // Equip/Bulk 구분: E 또는 B - // 패키지 정보 (스키마: packageCode, packageName) - packageCode: string - packageName: string - // 자재그룹 (스키마: materialGroupCode, materialGroupName) - materialGroupCode: string - materialGroupName: string - smCode: string // SM Code - similarMaterialNamePurchase: string // 유사자재명 (구매) - similarMaterialNameOther: string // 유사자재명 (구매 외) - // 협력업체 정보 (스키마: vendorCode, vendorName) - vendorCode: string - vendorName: string - taxId: string // 사업자번호(Tax ID) - faTarget: boolean // FA대상 - faStatus: string // FA현황 - faRemark: string // FA상세(Remark) - tier: string // 등급(Tier) - isAgent: boolean // Agent 여부 - // 계약서명주체 (스키마: contractSignerCode, contractSignerName) - contractSignerCode: string - contractSignerName: string - headquarterLocation: string // 본사 위치(국가) - manufacturingLocation: string // 제작/선적지(국가) - avlVendorName: string // AVL 등재업체명 - similarVendorName: string // 유사업체명(기술영업) - hasAvl: boolean // AVL: 존재여부 - isBlacklist: boolean // Blacklist - isBcc: boolean // BCC - purchaseOpinion: string // 구매의견 - // AVL 적용 선종(조선) - shipTypeCommon: boolean // 공통 - shipTypeAmax: boolean // A-max - shipTypeSmax: boolean // S-max - shipTypeVlcc: boolean // VLCC - shipTypeLngc: boolean // LNGC - shipTypeCont: boolean // CONT - // AVL 적용 선종(해양) - offshoreTypeCommon: boolean // 공통 - offshoreTypeFpso: boolean // FPSO - offshoreTypeFlng: boolean // FLNG - offshoreTypeFpu: boolean // FPU - offshoreTypePlatform: boolean // Platform - offshoreTypeWtiv: boolean // WTIV - offshoreTypeGom: boolean // GOM - // eVCP 미등록 정보 - picName: string // PIC(담당자) - picEmail: string // PIC(E-mail) - picPhone: string // PIC(Phone) - agentName: string // Agent(담당자) - agentEmail: string // Agent(E-mail) - agentPhone: string // Agent(Phone) - // 업체 실적 현황 - recentQuoteDate: string // 최근견적일 - recentQuoteNumber: string // 최근견적번호 - recentOrderDate: string // 최근발주일 - recentOrderNumber: string // 최근발주번호 - // 업데이트 히스토리 - registrationDate: string // 등재일 - registrant: string // 등재자 - lastModifiedDate: string // 최종변경일 - lastModifier: string // 최종변경자 +// Vendor Pool 데이터 타입 - 스키마 기반 + 테이블용 추가 필드 +import type { VendorPool } from "@/db/schema/avl/vendor-pool" +import { DisciplineCode, EngineeringDisciplineSelector } from "@/components/common/discipline" +import { MaterialGroupSelectorDialogSingle } from "@/components/common/material/material-group-selector-dialog-single" +import type { MaterialSearchItem } from "@/lib/material/material-group-service" +import { VendorTierSelector } from "@/components/common/selectors/vendor-tier/vendor-tier-selector" +import { VendorSelectorDialogSingle } from "@/components/common/vendor/vendor-selector-dialog-single" +import type { VendorSearchItem } from "@/components/common/vendor/vendor-service" +import { PlaceOfShippingSelectorDialogSingle } from "@/components/common/selectors/place-of-shipping/place-of-shipping-selector" + +export type VendorPoolItem = Omit & { + id: string | number // temp-로 시작하는 경우 string, 실제 데이터는 number + no: number // 테이블 표시용 순번 + selected: boolean // 테이블 선택 상태 + registrationDate: string // 표시용 string으로 변환 + lastModifiedDate: string // 표시용 string으로 변환 } // 테이블 컬럼 정의 export const columns: ColumnDef[] = [ - { - id: "select", - header: ({ table }) => ( - table.toggleAllPageRowsSelected(!!value)} - aria-label="Select all" - /> - ), - cell: ({ row }) => ( - row.toggleSelected(!!value)} - aria-label="Select row" - /> - ), - enableSorting: false, - enableHiding: false, - size: 40, - }, - { - accessorKey: "id", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const id = String(row.original.id) - - // 빈 행의 경우 No. 표시하지 않음 - if (id.startsWith('temp-')) { - return
신규
- } + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + size: 40, + }, + { + accessorKey: "id", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const id = String(row.original.id) + + // 빈 행의 경우 신규 표시 + if (id.startsWith('temp-')) { + return
신규
+ } - // vendor_pool 테이블의 실제 id 표시 - return
{id}
- }, - size: 60, - }, - { - accessorKey: "constructionSector", - header: ({ column }) => ( - 공사부문 *} /> - ), - cell: ({ row, table }) => { - const value = row.getValue("constructionSector") - 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, "constructionSector", newValue) - } - } + // 실제 ID 표시 + return
{id}
+ }, + size: 60, + }, + { + accessorKey: "constructionSector", + header: ({ column }) => ( + 공사부문 *} /> + ), + cell: ({ row, table }) => { + const value = row.getValue("constructionSector") + const isEmptyRow = String(row.original.id).startsWith('temp-') + + const onSave = async (newValue: any) => { + const meta = table.options.meta as VendorPoolTableMeta + if (meta?.onCellUpdate) { + await meta.onCellUpdate(row.original.id, "constructionSector", newValue) + } + } - // 수정 여부 확인 - const isModified = getIsModified(table, row.original.id, "constructionSector") - - return ( - - ) - }, - size: 100, - }, - { - accessorKey: "htDivision", - header: ({ column }) => ( - H/T구분 *} /> - ), - cell: ({ row, table }) => { - const value = row.getValue("htDivision") - 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, "htDivision", newValue) - } - } + // 수정 여부 확인 + const isModified = getIsModified(table, row.original.id, "constructionSector") - // 수정 여부 확인 - const isModified = getIsModified(table, row.original.id, "htDivision") - - return ( - - ) - }, - size: 80, - }, - { - accessorKey: "designCategoryCode", - header: ({ column }) => ( - - ), - cell: ({ row, table }) => { - const value = row.getValue("designCategoryCode") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "designCategoryCode", newValue) - } - } + return ( + + ) + }, + size: 100, + }, + { + accessorKey: "htDivision", + header: ({ column }) => ( + H/T *} /> + ), + cell: ({ row, table }) => { + const value = row.getValue("htDivision") + 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, "htDivision", newValue) + } + } - // 수정 여부 확인 - const isModified = getIsModified(table, row.original.id, "designCategoryCode") - - return ( - - ) - }, - size: 120, - }, - { - accessorKey: "designCategory", - header: ({ column }) => ( - 설계기능(공종) *} /> - ), - cell: ({ row, table }) => { - const value = row.getValue("designCategory") - 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, "designCategory", newValue) - } - } + // 수정 여부 확인 + const isModified = getIsModified(table, row.original.id, "htDivision") - // 수정 여부 확인 - const isModified = getIsModified(table, row.original.id, "designCategory") - - return ( - - ) - }, - size: 120, - }, - { - accessorKey: "equipBulkDivision", - header: ({ column }) => ( - - ), - cell: ({ row, table }) => { - const value = row.getValue("equipBulkDivision") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "equipBulkDivision", newValue) - } - } + return ( + + ) + }, + size: 80, + }, + { + accessorKey: "designCategory", + header: ({ column }) => ( + 설계기능(공종) *} /> + ), + cell: ({ row, table }) => { + const designCategoryCode = row.original.designCategoryCode as string + const designCategory = row.original.designCategory as string + + // 현재 선택된 discipline 구성 + const selectedDiscipline: DisciplineCode | undefined = designCategoryCode && designCategory ? { + CD: designCategoryCode, + USR_DF_CHAR_18: designCategory + } : undefined + + const onDisciplineSelect = async (discipline: DisciplineCode) => { + console.log('선택된 설계공종:', discipline) + console.log('행 ID:', row.original.id) + + // 설계기능코드와 설계기능(공종) 필드 모두 업데이트 + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "designCategoryCode", discipline.CD) + await table.options.meta.onCellUpdate(row.original.id, "designCategory", discipline.USR_DF_CHAR_18) + } else { + console.error('onCellUpdate가 정의되지 않음') + } + } - return ( - - ) - }, - size: 120, - }, - { - accessorKey: "packageCode", - header: ({ column }) => ( - - ), - cell: ({ row, table }) => { - const value = row.getValue("packageCode") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "packageCode", newValue) - } - } + return ( + + ) + }, + size: 260, + }, + { + accessorKey: "equipBulkDivision", + header: ({ column }) => ( + + ), + cell: ({ row, table }) => { + const value = row.getValue("equipBulkDivision") + 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, "packageCode") - - return ( - - ) - }, - size: 120, - }, - { - accessorKey: "packageName", - header: ({ column }) => ( - - ), - 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) - } - } + return ( + + ) + }, + size: 120, + }, + { + accessorKey: "packageCode", + header: ({ column }) => ( + + ), + cell: ({ row, table }) => { + const value = row.getValue("packageCode") + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "packageCode", newValue) + } + } - // 수정 여부 확인 - const isModified = getIsModified(table, row.original.id, "packageName") - - return ( - - ) - }, - size: 120, - }, - { - accessorKey: "materialGroupCode", - header: ({ column }) => ( - 자재그룹 코드 *} /> - ), - cell: ({ row, table }) => { - const value = row.getValue("materialGroupCode") - 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, "packageCode") - // 수정 여부 확인 - const isModified = getIsModified(table, row.original.id, "materialGroupCode") + return ( + + ) + }, + size: 120, + }, + { + accessorKey: "packageName", + header: ({ column }) => ( + + ), + 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 onChange = async (newValue: any) => { - if (table.options.meta?.onMaterialGroupCodeChange) { - await table.options.meta.onMaterialGroupCodeChange(row.original.id, newValue) - } - } + // 수정 여부 확인 + const isModified = getIsModified(table, row.original.id, "packageName") - return ( - - ) - }, - size: 120, - }, - { - accessorKey: "materialGroupName", - header: ({ column }) => ( - 자재그룹 명 *} /> - ), - 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) - } + return ( + + ) + }, + size: 200, + }, + { + accessorKey: "materialGroupName", + header: ({ column }) => ( + 자재그룹 *} /> + ), + cell: ({ row, table }) => { + const materialGroupCode = row.original.materialGroupCode as string + const materialGroupName = row.original.materialGroupName as string + + // 현재 선택된 material 구성 + const selectedMaterial: MaterialSearchItem | null = materialGroupCode && materialGroupName ? { + materialGroupCode, + materialGroupDescription: materialGroupName, + displayText: `${materialGroupCode} - ${materialGroupName}` + } : null + + const onMaterialSelect = async (material: MaterialSearchItem | null) => { + console.log('선택된 자재그룹:', material) + + if (material) { + // 자재그룹코드와 자재그룹명 필드 모두 업데이트 + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "materialGroupCode", material.materialGroupCode) + await table.options.meta.onCellUpdate(row.original.id, "materialGroupName", material.materialGroupDescription) } - - // 수정 여부 확인 - const isModified = getIsModified(table, row.original.id, "materialGroupName") - - return ( - - ) - }, - size: 120, - }, - { - accessorKey: "smCode", - header: ({ column }) => ( - - ), - cell: ({ row, table }) => { - const value = row.getValue("smCode") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "smCode", newValue) - } + } else { + // 선택 해제 시 빈 값으로 설정 + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "materialGroupCode", "") + await table.options.meta.onCellUpdate(row.original.id, "materialGroupName", "") } + } + } - return ( - - ) - }, - size: 100, - }, - { - accessorKey: "similarMaterialNamePurchase", - header: ({ column }) => ( - - ), - cell: ({ row, table }) => { - const value = row.getValue("similarMaterialNamePurchase") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "similarMaterialNamePurchase", newValue) - } - } + return ( + + ) + }, + size: 400, + }, + { + accessorKey: "smCode", + header: ({ column }) => ( + + ), + cell: ({ row, table }) => { + const value = row.getValue("smCode") + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "smCode", newValue) + } + } - return ( - - ) - }, - size: 140, - }, - { - accessorKey: "similarMaterialNameOther", - header: ({ column }) => ( - - ), - cell: ({ row, table }) => { - const value = row.getValue("similarMaterialNameOther") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "similarMaterialNameOther", newValue) - } - } + return ( + + ) + }, + size: 200, + }, + { + accessorKey: "similarMaterialNamePurchase", + header: ({ column }) => ( + + // 이전에는 컬럼명이 '유사자재명(구매외)' 였음. + ), + cell: ({ row, table }) => { + const value = row.getValue("similarMaterialNamePurchase") + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "similarMaterialNamePurchase", newValue) + } + } - return ( - - ) - }, - size: 140, - }, - { - accessorKey: "vendorCode", - header: ({ column }) => ( - - ), - cell: ({ row, table }) => { - const value = row.getValue("vendorCode") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "vendorCode", newValue) - } - } + return ( + + ) + }, + size: 250, + }, + { + accessorKey: "similarMaterialNameOther", + header: ({ column }) => ( + + ), + cell: ({ row, table }) => { + const value = row.getValue("similarMaterialNameOther") + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "similarMaterialNameOther", newValue) + } + } - // 수정 여부 확인 - const isModified = getIsModified(table, row.original.id, "vendorCode") - - return ( - - ) - }, - size: 130, - }, - { - accessorKey: "vendorName", - header: ({ column }) => ( - 협력업체 명 *} /> - ), - 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) - } + return ( + + ) + }, + size: 140, + }, + { + accessorKey: "vendorSelector", + header: ({ column }) => ( + + ), + cell: ({ row, table }) => { + const vendorCode = row.original.vendorCode as string + const vendorName = row.original.vendorName as string + + // 현재 선택된 vendor 구성 + const selectedVendor: VendorSearchItem | null = vendorCode && vendorName ? { + id: 0, // 실제로는 vendorId가 있어야 하지만 여기서는 임시로 0 사용 + vendorName, + vendorCode: vendorCode || null, + status: "ACTIVE", // 임시 값 + displayText: vendorName + (vendorCode ? ` (${vendorCode})` : "") + } : null + + const onVendorSelect = async (vendor: VendorSearchItem | null) => { + console.log('선택된 협력업체:', vendor) + + if (vendor) { + // 협력업체코드, 협력업체명, 사업자번호 필드 모두 업데이트 + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "vendorCode", vendor.vendorCode || "") + await table.options.meta.onCellUpdate(row.original.id, "vendorName", vendor.vendorName) + await table.options.meta.onCellUpdate(row.original.id, "taxId", vendor.taxId || "") } - - // 수정 여부 확인 - const isModified = getIsModified(table, row.original.id, "vendorName") - - return ( - - ) - }, - size: 130, - }, - { - accessorKey: "taxId", - header: ({ column }) => ( - 사업자번호 *} /> - ), - cell: ({ row, table }) => { - const value = row.getValue("taxId") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "taxId", newValue) - } + } else { + // 선택 해제 시 빈 값으로 설정 + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "vendorCode", "") + await table.options.meta.onCellUpdate(row.original.id, "vendorName", "") + await table.options.meta.onCellUpdate(row.original.id, "taxId", "") } + } + } - const onChange = async (newValue: any) => { - if (table.options.meta?.onTaxIdChange) { - await table.options.meta.onTaxIdChange(row.original.id, newValue) - } - } + return ( + + ) + }, + size: 150, + }, + { + accessorKey: "vendorCode", + header: ({ column }) => ( + + ), + cell: ({ row, table }) => { + const value = row.getValue("vendorCode") + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "vendorCode", newValue) + } + } - return ( - - ) - }, - size: 120, - }, - { - accessorKey: "faTarget", - header: "FA대상", - cell: ({ row, table }) => { - const value = row.getValue("faTarget") as boolean - 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, "vendorCode") - // 수정 여부 확인 - const isModified = getIsModified(table, row.original.id, "faTarget") - - return ( - - ) - }, - enableSorting: false, - size: 80, - }, - { - accessorKey: "faStatus", - header: ({ column }) => ( - - ), - cell: ({ row, table }) => { - const value = row.getValue("faStatus") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "faStatus", newValue) - } - } + return ( + + ) + }, + size: 130, + }, + { + accessorKey: "vendorName", + header: ({ column }) => ( + 협력업체 명 *} /> + ), + 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) + } + } - return ( - - ) - }, - size: 100, - }, - { - accessorKey: "faRemark", - header: ({ column }) => ( - - ), - cell: ({ row, table }) => { - const value = row.getValue("faRemark") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "faRemark", newValue) - } - } + // 수정 여부 확인 + const isModified = getIsModified(table, row.original.id, "vendorName") - return ( - - ) - }, - size: 120, - }, - { - accessorKey: "tier", - header: ({ column }) => ( - 등급 *} /> - ), - cell: ({ row, table }) => { - const value = row.getValue("tier") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "tier", newValue) - } - } + return ( + + ) + }, + size: 130, + }, + { + accessorKey: "taxId", + header: ({ column }) => ( + 사업자번호 *} /> + ), + cell: ({ row, table }) => { + const value = row.getValue("taxId") + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "taxId", newValue) + } + } - return ( - - ) - }, - size: 80, - }, - { - accessorKey: "isAgent", - header: "Agent 여부", - cell: ({ row, table }) => { - const value = row.getValue("isAgent") as boolean - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "isAgent", newValue) - } - } + const onChange = async (newValue: any) => { + if (table.options.meta?.onTaxIdChange) { + await table.options.meta.onTaxIdChange(row.original.id, newValue) + } + } - return ( - - ) - }, - enableSorting: false, - size: 100, - }, - { - accessorKey: "contractSignerCode", - header: ({ column }) => ( - - ), - cell: ({ row, table }) => { - const value = row.getValue("contractSignerCode") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "contractSignerCode", newValue) - } - } + return ( + + ) + }, + size: 120, + }, + { + accessorKey: "faTarget", + header: "FA대상", + cell: ({ row, table }) => { + const value = row.getValue("faTarget") as boolean + 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, "contractSignerCode") - - return ( - - ) - }, - size: 120, - }, - { - accessorKey: "contractSignerName", - header: ({ column }) => ( - 계약서명주체 명 *} /> - ), - cell: ({ row, table }) => { - const value = row.getValue("contractSignerName") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "contractSignerName", newValue) - } - } + // 수정 여부 확인 + const isModified = getIsModified(table, row.original.id, "faTarget") - // 수정 여부 확인 - const isModified = getIsModified(table, row.original.id, "contractSignerName") - - return ( - - ) - }, - size: 120, - }, - { - accessorKey: "headquarterLocation", - header: ({ column }) => ( - 본사 위치 *} /> - ), - cell: ({ row, table }) => { - const value = row.getValue("headquarterLocation") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "headquarterLocation", newValue) - } - } + return ( + + ) + }, + enableSorting: false, + size: 80, + }, + { + accessorKey: "faStatus", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const value = row.original.faStatus as string - return ( - - ) - }, - size: 100, - }, - { - accessorKey: "manufacturingLocation", - header: ({ column }) => ( - 제작/선적지 *} /> - ), - cell: ({ row, table }) => { - const value = row.getValue("manufacturingLocation") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "manufacturingLocation", newValue) - } - } + // 'O'인 경우에만 'O'를 표시, 그 외에는 빈 셀 + const displayValue = value === "O" ? "O" : "" - return ( - - ) - }, - size: 110, - }, - { - accessorKey: "avlVendorName", - header: ({ column }) => ( - AVL 등재업체명 *} /> - ), - cell: ({ row, table }) => { - const value = row.getValue("avlVendorName") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "avlVendorName", newValue) - } - } + return ( +
+ {displayValue} +
+ ) + }, + size: 120, + }, + // { + // accessorKey: "faRemark", + // header: ({ column }) => ( + // + // ), + // cell: ({ row, table }) => { + // const value = row.getValue("faRemark") + // const onSave = async (newValue: any) => { + // if (table.options.meta?.onCellUpdate) { + // await table.options.meta.onCellUpdate(row.original.id, "faRemark", newValue) + // } + // } + + // return ( + // + // ) + // }, + // size: 120, + // }, + { + accessorKey: "tier", + header: ({ column }) => ( + 등급 *} /> + ), + cell: ({ row, table }) => { + const value = row.original.tier as string - return ( - - ) - }, - size: 140, - }, - { - accessorKey: "similarVendorName", - header: ({ column }) => ( - - ), - cell: ({ row, table }) => { - const value = row.getValue("similarVendorName") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "similarVendorName", newValue) - } - } + const onValueChange = async (newValue: string) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "tier", newValue) + } + } - return ( - - ) - }, - size: 130, - }, - { - accessorKey: "hasAvl", - header: "AVL", - cell: ({ row, table }) => { - const value = row.getValue("hasAvl") as boolean - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "hasAvl", newValue) - } - } + return ( + + ) + }, + size: 200, + }, + { + accessorKey: "isAgent", + header: "Agent 여부", + cell: ({ row, table }) => { + const value = row.getValue("isAgent") as boolean + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "isAgent", newValue) + } + } - return ( - - ) - }, - enableSorting: false, - size: 60, - }, - { - accessorKey: "isBlacklist", - header: "Blacklist", - cell: ({ row, table }) => { - const value = row.getValue("isBlacklist") as boolean - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "isBlacklist", newValue) - } - } + return ( + + ) + }, + enableSorting: false, + size: 100, + }, + { + accessorKey: "contractSignerCode", + header: ({ column }) => ( + + ), + cell: ({ row, table }) => { + const value = row.getValue("contractSignerCode") + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "contractSignerCode", newValue) + } + } - return ( - - ) - }, - enableSorting: false, - size: 100, - }, - { - accessorKey: "isBcc", - header: "BCC", - cell: ({ row, table }) => { - const value = row.getValue("isBcc") as boolean - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "isBcc", newValue) - } - } + // 수정 여부 확인 + const isModified = getIsModified(table, row.original.id, "contractSignerCode") - return ( - - ) - }, - enableSorting: false, - size: 80, - }, - { - accessorKey: "purchaseOpinion", - header: ({ column }) => ( - - ), - cell: ({ row, table }) => { - const value = row.getValue("purchaseOpinion") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "purchaseOpinion", newValue) - } + return ( + + ) + }, + size: 120, + }, + { + accessorKey: "contractSignerSelector", + header: ({ column }) => ( + + ), + cell: ({ row, table }) => { + const contractSignerCode = row.original.contractSignerCode as string + const contractSignerName = row.original.contractSignerName as string + + // 현재 선택된 contract signer 구성 + const selectedVendor: VendorSearchItem | null = contractSignerCode && contractSignerName ? { + id: 0, // 실제로는 vendorId가 있어야 하지만 여기서는 임시로 0 사용 + vendorName: contractSignerName, + vendorCode: contractSignerCode || null, + status: "ACTIVE", // 임시 값 + displayText: contractSignerName + (contractSignerCode ? ` (${contractSignerCode})` : "") + } : null + + const onVendorSelect = async (vendor: VendorSearchItem | null) => { + console.log('선택된 계약서명주체:', vendor) + + if (vendor) { + // 계약서명주체코드와 계약서명주체명 필드 업데이트 + // 사업자번호는 협력업체 선택 시에만 업데이트됨 (taxId 필드가 하나만 존재) + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "contractSignerCode", vendor.vendorCode || "") + await table.options.meta.onCellUpdate(row.original.id, "contractSignerName", vendor.vendorName) } - - return ( - - ) - }, - size: 120, - }, - { - accessorKey: "shipTypeCommon", - header: "공통", - cell: ({ row, table }) => { - const value = row.getValue("shipTypeCommon") as boolean - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "shipTypeCommon", newValue) - } + } else { + // 선택 해제 시 빈 값으로 설정 + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "contractSignerCode", "") + await table.options.meta.onCellUpdate(row.original.id, "contractSignerName", "") } + } + } - return ( - - ) - }, - enableSorting: false, - size: 80, - }, - { - accessorKey: "shipTypeAmax", - header: "A-max", - cell: ({ row, table }) => { - const value = row.getValue("shipTypeAmax") as boolean - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "shipTypeAmax", newValue) - } - } + return ( + + ) + }, + size: 150, + }, + { + accessorKey: "contractSignerName", + header: ({ column }) => ( + 계약서명주체 명 *} /> + ), + cell: ({ row, table }) => { + const value = row.getValue("contractSignerName") + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "contractSignerName", newValue) + } + } - return ( - - ) - }, - enableSorting: false, - size: 80, - }, - { - accessorKey: "shipTypeSmax", - header: "S-max", - cell: ({ row, table }) => { - const value = row.getValue("shipTypeSmax") as boolean - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "shipTypeSmax", newValue) - } - } + // 수정 여부 확인 + const isModified = getIsModified(table, row.original.id, "contractSignerName") - return ( - - ) - }, - enableSorting: false, - size: 80, - }, - { - accessorKey: "shipTypeVlcc", - header: "VLCC", - cell: ({ row, table }) => { - const value = row.getValue("shipTypeVlcc") as boolean - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "shipTypeVlcc", newValue) - } - } + return ( + + ) + }, + size: 120, + }, + { + accessorKey: "headquarterLocation", + header: ({ column }) => ( + 본사 위치 *} /> + ), + cell: ({ row, table }) => { + const value = row.getValue("headquarterLocation") + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "headquarterLocation", newValue) + } + } - return ( - - ) - }, - enableSorting: false, - size: 80, - }, - { - accessorKey: "shipTypeLngc", - header: "LNGC", - cell: ({ row, table }) => { - const value = row.getValue("shipTypeLngc") as boolean - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "shipTypeLngc", newValue) - } - } + return ( + + ) + }, + size: 100, + }, + { + accessorKey: "manufacturingLocation", + header: ({ column }) => ( + 제작/선적지 *} /> + ), + cell: ({ row, table }) => { + const manufacturingLocation = row.original.manufacturingLocation as string - return ( - - ) - }, - size: 80, - }, - { - accessorKey: "shipTypeCont", - header: "CONT", - cell: ({ row, table }) => { - const value = row.getValue("shipTypeCont") as boolean - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "shipTypeCont", newValue) - } - } + // 현재 선택된 장소 구성 (description은 알 수 없으므로 null로 설정) + const selectedPlace = null // 선택된 장소 표시를 위해 null로 설정 - return ( - - ) - }, - size: 80, - }, - { - accessorKey: "offshoreTypeCommon", - header: "공통", - cell: ({ row, table }) => { - const value = row.getValue("offshoreTypeCommon") as boolean - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeCommon", newValue) - } - } + const onPlaceSelect = async (place: { code: string; description: string } | null) => { + console.log('선택된 제작/선적지:', place) - return ( - - ) - }, - size: 80, - }, - { - accessorKey: "offshoreTypeFpso", - header: "FPSO", - cell: ({ row, table }) => { - const value = row.getValue("offshoreTypeFpso") as boolean - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeFpso", newValue) - } - } + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "manufacturingLocation", place?.code || "") + } + } - return ( - - ) - }, - size: 80, - }, - { - accessorKey: "offshoreTypeFlng", - header: "FLNG", - cell: ({ row, table }) => { - const value = row.getValue("offshoreTypeFlng") as boolean - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeFlng", newValue) - } - } + return ( + + ) + }, + size: 200, + }, + { + accessorKey: "avlVendorName", + header: ({ column }) => ( + AVL 등재업체명 *} /> + ), + cell: ({ row, table }) => { + const value = row.getValue("avlVendorName") + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "avlVendorName", newValue) + } + } - return ( - - ) - }, - size: 80, - }, - { - accessorKey: "offshoreTypeFpu", - header: "FPU", - cell: ({ row, table }) => { - const value = row.getValue("offshoreTypeFpu") as boolean - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeFpu", newValue) - } - } + return ( + + ) + }, + size: 140, + }, + { + accessorKey: "similarVendorName", + header: ({ column }) => ( + + // 이전에는 컬럼명이 '유사업체명' 였음. + ), + cell: ({ row, table }) => { + const value = row.getValue("similarVendorName") + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "similarVendorName", newValue) + } + } - return ( - - ) - }, - size: 80, - }, - { - accessorKey: "offshoreTypePlatform", - header: "Platform", - cell: ({ row, table }) => { - const value = row.getValue("offshoreTypePlatform") as boolean - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "offshoreTypePlatform", newValue) - } - } + return ( + + ) + }, + size: 130, + }, + { + accessorKey: "hasAvl", + header: "AVL", + cell: ({ row, table }) => { + const value = row.getValue("hasAvl") as boolean + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "hasAvl", newValue) + } + } - return ( - - ) - }, - size: 100, - }, - { - accessorKey: "offshoreTypeWtiv", - header: "WTIV", - cell: ({ row, table }) => { - const value = row.getValue("offshoreTypeWtiv") as boolean - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeWtiv", newValue) - } - } + return ( + + ) + }, + enableSorting: false, + size: 60, + }, + { + accessorKey: "isBlacklist", + header: "Blacklist", + cell: ({ row, table }) => { + const value = row.getValue("isBlacklist") as boolean + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "isBlacklist", newValue) + } + } - return ( - - ) - }, - size: 80, - }, - { - accessorKey: "offshoreTypeGom", - header: "GOM", - cell: ({ row, table }) => { - const value = row.getValue("offshoreTypeGom") as boolean - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeGom", newValue) - } - } + return ( + + ) + }, + enableSorting: false, + size: 60, + }, + { + accessorKey: "isBcc", + header: "BCC", + cell: ({ row, table }) => { + const value = row.getValue("isBcc") as boolean + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "isBcc", newValue) + } + } - return ( - - ) - }, - size: 80, - }, - { - accessorKey: "picName", - header: ({ column }) => ( - - ), - cell: ({ row, table }) => { - const value = row.getValue("picName") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "picName", newValue) - } - } + return ( + + ) + }, + enableSorting: false, + size: 60, + }, + { + accessorKey: "purchaseOpinion", + header: ({ column }) => ( + + ), + cell: ({ row, table }) => { + const value = row.getValue("purchaseOpinion") + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "purchaseOpinion", newValue) + } + } - return ( - - ) - }, - size: 120, - }, - { - accessorKey: "picEmail", - header: ({ column }) => ( - - ), - cell: ({ row, table }) => { - const value = row.getValue("picEmail") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "picEmail", newValue) - } - } - return ( - - ) - }, - size: 140, - }, - { - accessorKey: "picPhone", - header: ({ column }) => ( - - ), - cell: ({ row, table }) => { - const value = row.getValue("picPhone") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "picPhone", newValue) - } - } + return ( + + ) + }, + size: 300, + }, + { + accessorKey: "shipTypeCommon", + header: "공통", + cell: ({ row, table }) => { + const value = row.getValue("shipTypeCommon") as boolean + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "shipTypeCommon", newValue) + } + } - return ( - - ) - }, - size: 120, - }, - { - accessorKey: "agentName", - header: ({ column }) => ( - - ), - cell: ({ row, table }) => { - const value = row.getValue("agentName") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "agentName", newValue) - } - } + return ( + + ) + }, + enableSorting: false, + size: 80, + }, + { + accessorKey: "shipTypeAmax", + header: "A-max", + cell: ({ row, table }) => { + const value = row.getValue("shipTypeAmax") as boolean + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "shipTypeAmax", newValue) + } + } - return ( - - ) - }, - size: 120, - }, - { - accessorKey: "agentEmail", - header: ({ column }) => ( - - ), - cell: ({ row, table }) => { - const value = row.getValue("agentEmail") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "agentEmail", newValue) - } - } + return ( + + ) + }, + enableSorting: false, + size: 60, + }, + { + accessorKey: "shipTypeSmax", + header: "S-max", + cell: ({ row, table }) => { + const value = row.getValue("shipTypeSmax") as boolean + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "shipTypeSmax", newValue) + } + } - return ( - - ) - }, - size: 140, - }, - { - accessorKey: "agentPhone", - header: ({ column }) => ( - - ), - cell: ({ row, table }) => { - const value = row.getValue("agentPhone") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "agentPhone", newValue) - } - } + return ( + + ) + }, + enableSorting: false, + size: 60, + }, + { + accessorKey: "shipTypeVlcc", + header: "VLCC", + cell: ({ row, table }) => { + const value = row.getValue("shipTypeVlcc") as boolean + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "shipTypeVlcc", newValue) + } + } - return ( - - ) - }, - size: 120, - }, - { - accessorKey: "recentQuoteDate", - header: ({ column }) => ( - - ), - cell: ({ row, table }) => { - const value = row.getValue("recentQuoteDate") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "recentQuoteDate", newValue) - } - } + return ( + + ) + }, + enableSorting: false, + size: 60, + }, + { + accessorKey: "shipTypeLngc", + header: "LNGC", + cell: ({ row, table }) => { + const value = row.getValue("shipTypeLngc") as boolean + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "shipTypeLngc", newValue) + } + } - return ( - - ) - }, - size: 120, - }, - { - accessorKey: "recentQuoteNumber", - header: ({ column }) => ( - - ), - cell: ({ row, table }) => { - const value = row.getValue("recentQuoteNumber") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "recentQuoteNumber", newValue) - } - } + return ( + + ) + }, + size: 60, + }, + { + accessorKey: "shipTypeCont", + header: "CONT", + cell: ({ row, table }) => { + const value = row.getValue("shipTypeCont") as boolean + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "shipTypeCont", newValue) + } + } - return ( - - ) - }, - size: 130, - }, - { - accessorKey: "recentOrderDate", - header: ({ column }) => ( - - ), - cell: ({ row, table }) => { - const value = row.getValue("recentOrderDate") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "recentOrderDate", newValue) - } - } + return ( + + ) + }, + size: 60, + }, + { + accessorKey: "offshoreTypeCommon", + header: "공통", + cell: ({ row, table }) => { + const value = row.getValue("offshoreTypeCommon") as boolean + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeCommon", newValue) + } + } - return ( - - ) - }, - size: 120, - }, - { - accessorKey: "recentOrderNumber", - header: ({ column }) => ( - - ), - cell: ({ row, table }) => { - const value = row.getValue("recentOrderNumber") - const onSave = async (newValue: any) => { - if (table.options.meta?.onCellUpdate) { - await table.options.meta.onCellUpdate(row.original.id, "recentOrderNumber", newValue) - } - } + return ( + + ) + }, + size: 60, + }, + { + accessorKey: "offshoreTypeFpso", + header: "FPSO", + cell: ({ row, table }) => { + const value = row.getValue("offshoreTypeFpso") as boolean + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeFpso", newValue) + } + } + + return ( + + ) + }, + size: 60, + }, + { + accessorKey: "offshoreTypeFlng", + header: "FLNG", + cell: ({ row, table }) => { + const value = row.getValue("offshoreTypeFlng") as boolean + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeFlng", newValue) + } + } + + return ( + + ) + }, + size: 60, + }, + { + accessorKey: "offshoreTypeFpu", + header: "FPU", + cell: ({ row, table }) => { + const value = row.getValue("offshoreTypeFpu") as boolean + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeFpu", newValue) + } + } + + return ( + + ) + }, + size: 60, + }, + { + accessorKey: "offshoreTypePlatform", + header: "Platform", + cell: ({ row, table }) => { + const value = row.getValue("offshoreTypePlatform") as boolean + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "offshoreTypePlatform", newValue) + } + } + + return ( + + ) + }, + size: 60, + }, + { + accessorKey: "offshoreTypeWtiv", + header: "WTIV", + cell: ({ row, table }) => { + const value = row.getValue("offshoreTypeWtiv") as boolean + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeWtiv", newValue) + } + } + + return ( + + ) + }, + size: 60, + }, + { + accessorKey: "offshoreTypeGom", + header: "GOM", + cell: ({ row, table }) => { + const value = row.getValue("offshoreTypeGom") as boolean + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeGom", newValue) + } + } + + return ( + + ) + }, + size: 60, + }, + { + accessorKey: "picName", + header: ({ column }) => ( + + // 이전에는 컬럼명이 PIC(담당자) 였음. + ), + cell: ({ row, table }) => { + const value = row.getValue("picName") + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "picName", newValue) + } + } + + return ( + + ) + }, + size: 120, + }, + { + accessorKey: "picEmail", + header: ({ column }) => ( + + // 이전에는 컬럼명이 PIC(E-mail) 였음. + ), + cell: ({ row, table }) => { + const value = row.getValue("picEmail") + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "picEmail", newValue) + } + } + return ( + + ) + }, + size: 140, + }, + { + accessorKey: "picPhone", + header: ({ column }) => ( + + // 이전에는 컬럼명이 PIC(Phone) 였음. + ), + cell: ({ row, table }) => { + const value = row.getValue("picPhone") + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "picPhone", newValue) + } + } + + return ( + + ) + }, + size: 120, + }, + { + accessorKey: "agentName", + header: ({ column }) => ( + + ), + cell: ({ row, table }) => { + const value = row.getValue("agentName") + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "agentName", newValue) + } + } + + return ( + + ) + }, + size: 120, + }, + { + accessorKey: "agentEmail", + header: ({ column }) => ( + + ), + cell: ({ row, table }) => { + const value = row.getValue("agentEmail") + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "agentEmail", newValue) + } + } - return ( - - ) - }, - size: 130, - }, - { - accessorKey: "registrationDate", - header: ({ column }) => ( - - ), - size: 120, - }, - { - accessorKey: "registrant", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const value = row.getValue("registrant") as string - return
{value || ""}
- }, - size: 100, - }, - { - accessorKey: "lastModifiedDate", - header: ({ column }) => ( - - ), - size: 120, - }, - { - accessorKey: "lastModifier", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const value = row.getValue("lastModifier") as string - return
{value || ""}
- }, - size: 120, - }, + return ( + + ) + }, + size: 140, + }, + { + accessorKey: "agentPhone", + header: ({ column }) => ( + + ), + cell: ({ row, table }) => { + const value = row.getValue("agentPhone") + const onSave = async (newValue: any) => { + if (table.options.meta?.onCellUpdate) { + await table.options.meta.onCellUpdate(row.original.id, "agentPhone", newValue) + } + } + + return ( + + ) + }, + size: 120, + }, + { + accessorKey: "recentQuoteDate", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const value = row.getValue("recentQuoteDate") as string + return ( +
+ {value || "-"} +
+ ) + }, + size: 120, + }, + { + accessorKey: "recentQuoteNumber", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const value = row.getValue("recentQuoteNumber") as string + return ( +
+ {value || "-"} +
+ ) + }, + size: 130, + }, + { + accessorKey: "recentOrderDate", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const value = row.getValue("recentOrderDate") as string + return ( +
+ {value || "-"} +
+ ) + }, + size: 120, + }, + { + accessorKey: "recentOrderNumber", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const value = row.getValue("recentOrderNumber") as string + return ( +
+ {value || "-"} +
+ ) + }, + size: 130, + }, + { + accessorKey: "registrationDate", + header: ({ column }) => ( + + ), + size: 120, + }, + { + accessorKey: "registrant", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const value = row.getValue("registrant") as string + return
{value || ""}
+ }, + size: 100, + }, + { + accessorKey: "lastModifiedDate", + header: ({ column }) => ( + + ), + size: 120, + }, + { + accessorKey: "lastModifier", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const value = row.getValue("lastModifier") as string + return
{value || ""}
+ }, + size: 120, + }, // 액션 그룹 { id: "actions", diff --git a/lib/vendor-pool/table/vendor-pool-table.tsx b/lib/vendor-pool/table/vendor-pool-table.tsx index 43dd64c1..46a0588d 100644 --- a/lib/vendor-pool/table/vendor-pool-table.tsx +++ b/lib/vendor-pool/table/vendor-pool-table.tsx @@ -16,19 +16,20 @@ import { BulkImportDialog } from "./bulk-import-dialog" import { columns, type VendorPoolItem } from "./vendor-pool-table-columns" import { createVendorPool, updateVendorPool, deleteVendorPool } from "../service" -import type { VendorPool } from "../types" - -// 테이블 메타 타입 확장 -declare module "@tanstack/react-table" { - interface TableMeta { - onCellUpdate?: (id: string, field: keyof TData, newValue: any) => Promise - onCellCancel?: (id: string, field: keyof TData) => void - onAction?: (action: string, data?: any) => void - onSaveEmptyRow?: (tempId: string) => Promise - onCancelEmptyRow?: (tempId: string) => void - isEmptyRow?: (id: string) => boolean - getPendingChanges?: () => Record> - } +import type { VendorPool } from "@/db/schema/avl/vendor-pool" +import { Download, FileSpreadsheet, Upload } from "lucide-react" +import { ImportVendorPoolButton } from "./vendor-pool-excel-import-button" +import { exportVendorPoolToExcel, createVendorPoolTemplate } from "../excel-utils" + +// vendor-pool 테이블 메타 타입 +interface VendorPoolTableMeta { + onCellUpdate?: (id: string | number, field: string, newValue: any) => Promise + onCellCancel?: (id: string | number, field: string) => void + onAction?: (action: string, data?: any) => void + onSaveEmptyRow?: (tempId: string) => Promise + onCancelEmptyRow?: (tempId: string) => void + isEmptyRow?: (id: string) => boolean + getPendingChanges?: () => Record> } interface VendorPoolTableProps { @@ -37,6 +38,67 @@ interface VendorPoolTableProps { onRefresh?: () => void // 데이터 새로고침 콜백 } +// 빈 행 기본값 객체 +const createEmptyVendorPoolBase = (): Omit & { id?: string | number } => ({ + constructionSector: "", + htDivision: "", + designCategoryCode: "", + designCategory: "", + equipBulkDivision: "", + packageCode: null, + packageName: null, + materialGroupCode: null, + materialGroupName: null, + smCode: null, + similarMaterialNamePurchase: null, + similarMaterialNameOther: null, + vendorCode: null, + vendorName: "", + taxId: null, + faTarget: false, + faStatus: null, + faRemark: null, + tier: null, + isAgent: false, + contractSignerCode: null, + contractSignerName: "", + headquarterLocation: "", + manufacturingLocation: "", + avlVendorName: null, + similarVendorName: null, + hasAvl: false, + isBlacklist: false, + isBcc: false, + purchaseOpinion: null, + shipTypeCommon: false, + shipTypeAmax: false, + shipTypeSmax: false, + shipTypeVlcc: false, + shipTypeLngc: false, + shipTypeCont: false, + offshoreTypeCommon: false, + offshoreTypeFpso: false, + offshoreTypeFlng: false, + offshoreTypeFpu: false, + offshoreTypePlatform: false, + offshoreTypeWtiv: false, + offshoreTypeGom: false, + picName: null, + picEmail: null, + picPhone: null, + agentName: null, + agentEmail: null, + agentPhone: null, + recentQuoteDate: null, + recentQuoteNumber: null, + recentOrderDate: null, + recentOrderNumber: null, + registrationDate: null, + registrant: null, + lastModifiedDate: null, + lastModifier: null, +}) + export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableProps) { const { data: session } = useSession() @@ -54,7 +116,7 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP // 인라인 편집 핸들러 (일괄 저장용) - const handleCellUpdate = React.useCallback(async (id: string, field: keyof VendorPoolItem, newValue: any) => { + const handleCellUpdate = React.useCallback(async (id: string | number, field: string, newValue: any) => { const isEmptyRow = String(id).startsWith('temp-') if (isEmptyRow) { @@ -81,7 +143,7 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP // 편집 취소 핸들러 - const handleCellCancel = React.useCallback((id: string, field: keyof VendorPoolItem) => { + const handleCellCancel = React.useCallback((id: string | number, field: string) => { const isEmptyRow = String(id).startsWith('temp-') if (isEmptyRow) { @@ -142,13 +204,13 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP for (const [id, changes] of Object.entries(pendingChanges)) { try { // changes에서 id 필드 제거 (서버에서 자동 생성) - const { id: _, ...updateData } = changes as any + const { id: _, no: __, selected: ___, ...updateData } = changes // 최종변경자를 현재 세션 사용자 정보로 설정 - const updateDataWithModifier = { + const updateDataWithModifier: any = { ...updateData, - lastModifier: session?.user?.name || "" + lastModifier: session?.user?.name || null } - const result = await updateVendorPool(Number(id), updateDataWithModifier as Partial) + const result = await updateVendorPool(Number(id), updateDataWithModifier) if (result) { successCount++ } else { @@ -190,68 +252,18 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP if (isCreating) return // 이미 생성 중이면 중복 생성 방지 const tempId = `temp-${Date.now()}` + const userName = session?.user?.name || null + const emptyRow: VendorPoolItem = { - id: tempId, + ...createEmptyVendorPoolBase(), + id: tempId, // string 타입으로 설정 no: 0, // 나중에 계산 selected: false, - constructionSector: "", - htDivision: "", - designCategoryCode: "", - designCategory: "", - equipBulkDivision: "", - packageCode: "", - packageName: "", - materialGroupCode: "", - materialGroupName: "", - smCode: "", - similarMaterialNamePurchase: "", - similarMaterialNameOther: "", - vendorCode: "", - vendorName: "", - taxId: "", - faTarget: false, - faStatus: "", - faRemark: "", - tier: "", - isAgent: false, - contractSignerCode: "", - contractSignerName: "", - headquarterLocation: "", - manufacturingLocation: "", - avlVendorName: "", - similarVendorName: "", - hasAvl: false, - isBlacklist: false, - isBcc: false, - purchaseOpinion: "", - shipTypeCommon: false, - shipTypeAmax: false, - shipTypeSmax: false, - shipTypeVlcc: false, - shipTypeLngc: false, - shipTypeCont: false, - offshoreTypeCommon: false, - offshoreTypeFpso: false, - offshoreTypeFlng: false, - offshoreTypeFpu: false, - offshoreTypePlatform: false, - offshoreTypeWtiv: false, - offshoreTypeGom: false, - picName: "", - picEmail: "", - picPhone: "", - agentName: "", - agentEmail: "", - agentPhone: "", - recentQuoteDate: "", - recentQuoteNumber: "", - recentOrderDate: "", - recentOrderNumber: "", - registrationDate: "", - registrant: session?.user?.name || "", + registrationDate: "", // 빈 행에서는 string으로 표시 + registrant: userName, lastModifiedDate: "", - lastModifier: session?.user?.name || "", - } + lastModifier: userName, + } as unknown as VendorPoolItem setEmptyRows(prev => ({ ...prev, [tempId]: emptyRow })) setIsCreating(true) @@ -312,10 +324,10 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP try { setIsSaving(true) - // id 필드 제거 (서버에서 자동 생성) - const { id: _, no: __, selected: ___, ...createData } = finalData + // id, no, selected 필드 제거 및 타입 변환 + const { id: _, no: __, selected: ___, registrationDate: ____, lastModifiedDate: _____, ...createData } = finalData - const result = await createVendorPool(createData as Omit) + const result = await createVendorPool(createData as any) if (result) { toast.success("새 항목이 추가되었습니다.") @@ -591,8 +603,34 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP toast.info('저장 기능은 개발 중입니다.') break - case 'excel-import': - toast.info('Excel Import 기능은 개발 중입니다.') + + case 'excel-export': + try { + // 현재 테이블 데이터를 Excel로 내보내기 (ID 포함) + const currentData = table.getFilteredRowModel().rows.map(row => row.original) + await exportVendorPoolToExcel( + currentData, + `vendor-pool-${new Date().toISOString().split('T')[0]}.xlsx`, + true // ID 포함 + ) + toast.success('Excel 파일이 다운로드되었습니다.') + } catch (error) { + console.error('Excel export 실패:', error) + toast.error('Excel 내보내기에 실패했습니다.') + } + break + + case 'excel-template': + try { + // 템플릿 파일 다운로드 (데이터 없음, ID 컬럼 제외) + await createVendorPoolTemplate( + `vendor-pool-template-${new Date().toISOString().split('T')[0]}.xlsx` + ) + toast.success('Excel 템플릿이 다운로드되었습니다.') + } catch (error) { + console.error('Excel template export 실패:', error) + toast.error('Excel 템플릿 다운로드에 실패했습니다.') + } break case 'delete': @@ -634,7 +672,7 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP // 제공된 값들만 적용 (빈 값이나 undefined는 건너뜀) Object.entries(bulkData).forEach(([field, value]) => { if (value !== undefined && value !== null && value !== '') { - handleCellUpdate(rowId, field as keyof VendorPoolItem, value) + handleCellUpdate(rowId, field as keyof VendorPool, value) } }) } @@ -648,7 +686,7 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP }, [table, handleCellUpdate]) // 테이블 메타에 핸들러 설정 - table.options.meta = { + const tableMeta: VendorPoolTableMeta = { onAction: handleAction, onCellUpdate: handleCellUpdate, onCellCancel: handleCellCancel, @@ -657,6 +695,8 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP isEmptyRow: (id: string) => String(id).startsWith('temp-'), getPendingChanges: () => pendingChanges } + + table.options.meta = tableMeta as any // 툴바 액션 핸들러들 @@ -699,12 +739,24 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP 일괄입력 + + + +