diff options
| author | joonhoekim <26rote@gmail.com> | 2025-11-05 16:07:43 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-11-05 16:07:43 +0900 |
| commit | 208ed7ff11d0f822d3d243c5833d31973904349e (patch) | |
| tree | b1a15e3a7f8242294397f433a8484102c5743e69 | |
| parent | ad6bde0250cfe014d5f78747ec76ac59df95a25d (diff) | |
(김준회) vendor-pool: excel-import 코드기반검색처리, 템플릿 변경 (구매 김수진 프로 요청사항)
| -rw-r--r-- | components/common/vendor/vendor-service.ts | 38 | ||||
| -rw-r--r-- | db/schema/avl/vendor-pool.ts | 2 | ||||
| -rw-r--r-- | lib/material/material-group-service.ts | 38 | ||||
| -rw-r--r-- | lib/vendor-pool/enrichment-service.ts | 140 | ||||
| -rw-r--r-- | lib/vendor-pool/excel-utils.ts | 55 | ||||
| -rw-r--r-- | lib/vendor-pool/service.ts | 11 | ||||
| -rw-r--r-- | lib/vendor-pool/table/import-progress-dialog.tsx | 54 | ||||
| -rw-r--r-- | lib/vendor-pool/table/import-result-dialog.tsx | 167 | ||||
| -rw-r--r-- | lib/vendor-pool/table/vendor-pool-excel-import-button.tsx | 322 | ||||
| -rw-r--r-- | lib/vendor-pool/table/vendor-pool-table-columns.tsx | 40 | ||||
| -rw-r--r-- | lib/vendor-pool/table/vendor-pool-table.tsx | 3 | ||||
| -rw-r--r-- | lib/vendor-pool/types.ts | 1 | ||||
| -rw-r--r-- | lib/vendor-pool/validations.ts | 1 |
13 files changed, 769 insertions, 103 deletions
diff --git a/components/common/vendor/vendor-service.ts b/components/common/vendor/vendor-service.ts index 1c59843c..8c440df2 100644 --- a/components/common/vendor/vendor-service.ts +++ b/components/common/vendor/vendor-service.ts @@ -199,6 +199,44 @@ export async function getAllVendors(): Promise<{ } /** + * 벤더 코드로 조회 + */ +export async function getVendorByCode(vendorCode: string): Promise<VendorSearchItem | null> { + if (!vendorCode.trim()) { + return null + } + + try { + const result = await db + .select({ + id: vendors.id, + vendorName: vendors.vendorName, + vendorCode: vendors.vendorCode, + taxId: vendors.taxId, + status: vendors.status, + }) + .from(vendors) + .where(eq(vendors.vendorCode, vendorCode.trim())) + .limit(1) + + if (result.length === 0) { + return null + } + + const vendor = result[0] + return { + ...vendor, + displayText: vendor.vendorCode + ? `${vendor.vendorName} (${vendor.vendorCode})` + : vendor.vendorName + } + } catch (error) { + console.error('Error fetching vendor by code:', error) + return null + } +} + +/** * 특정 벤더 조회 (ID로) */ export async function getVendorById(vendorId: number): Promise<VendorSearchItem | null> { diff --git a/db/schema/avl/vendor-pool.ts b/db/schema/avl/vendor-pool.ts index a1b7fa3a..9f2cdd1a 100644 --- a/db/schema/avl/vendor-pool.ts +++ b/db/schema/avl/vendor-pool.ts @@ -15,7 +15,7 @@ export const vendorPool = pgTable("vendor_pool", { // 설계 정보 designCategoryCode: varchar("design_category_code", { length: 2 }).notNull(), // 2자리 영문대문자 designCategory: varchar("design_category", { length: 50 }).notNull(), // 전장 등 - equipBulkDivision: varchar("equip_bulk_division", { length: 1 }).notNull(), // E 또는 B + equipBulkDivision: varchar("equip_bulk_division", { length: 1 }), // E 또는 B // 패키지 정보 packageCode: varchar("package_code", { length: 50 }), // 패키지 코드 diff --git a/lib/material/material-group-service.ts b/lib/material/material-group-service.ts index 41f06fac..902d9b9b 100644 --- a/lib/material/material-group-service.ts +++ b/lib/material/material-group-service.ts @@ -25,6 +25,44 @@ export interface MaterialSearchResult { } /** + * 자재 그룹 코드로 단일 조회 + */ +export async function getMaterialGroupByCode( + materialGroupCode: string +): Promise<MaterialSearchItem | null> { + if (!materialGroupCode.trim()) { + return null; + } + + try { + const result = await db + .select({ + materialGroupCode: MATERIAL_GROUP_MASTER.materialGroupCode, + materialGroupDescription: MATERIAL_GROUP_MASTER.materialGroupDescription, + materialGroupUom: MATERIAL_GROUP_MASTER.materialGroupUom, + }) + .from(MATERIAL_GROUP_MASTER) + .where(sql`${MATERIAL_GROUP_MASTER.materialGroupCode} = ${materialGroupCode.trim()}`) + .limit(1); + + if (result.length === 0) { + return null; + } + + const item = result[0]; + return { + materialGroupCode: item.materialGroupCode, + materialGroupDescription: item.materialGroupDescription, + materialGroupUom: item.materialGroupUom || undefined, + displayText: `${item.materialGroupCode} - ${item.materialGroupDescription}`, + }; + } catch (error) { + console.error("자재 그룹 코드 조회 오류:", error); + return null; + } +} + +/** * 자재 검색 함수 - material_search_view에서 검색 */ export async function searchMaterialsForSelector( diff --git a/lib/vendor-pool/enrichment-service.ts b/lib/vendor-pool/enrichment-service.ts new file mode 100644 index 00000000..88694227 --- /dev/null +++ b/lib/vendor-pool/enrichment-service.ts @@ -0,0 +1,140 @@ +"use server"; + +import { getDisciplineCodeByCode } from "@/components/common/discipline/discipline-service"; +import { getMaterialGroupByCode } from "@/lib/material/material-group-service"; +import { getVendorByCode } from "@/components/common/vendor/vendor-service"; +import { debugLog, debugWarn, debugSuccess } from "@/lib/debug-utils"; + +/** + * 엑셀에서 가져온 벤더풀 데이터를 enrichment (자동완성) + * 코드 필드를 기반으로 나머지 데이터를 자동으로 채웁니다. + */ +export interface VendorPoolEnrichmentInput { + // 설계기능 관련 + designCategoryCode?: string; + designCategory?: string; + + // 자재그룹 관련 + materialGroupCode?: string; + materialGroupName?: string; + + // 협력업체 관련 + vendorCode?: string; + vendorName?: string; + + // 계약서명주체 관련 + contractSignerCode?: string; + contractSignerName?: string; +} + +export interface VendorPoolEnrichmentResult { + enriched: VendorPoolEnrichmentInput; + enrichedFields: string[]; // 자동완성된 필드 목록 + warnings: string[]; // 경고 메시지 (코드는 있지만 데이터를 찾을 수 없는 경우) +} + +/** + * 벤더풀 데이터 enrichment + */ +export async function enrichVendorPoolData( + data: VendorPoolEnrichmentInput +): Promise<VendorPoolEnrichmentResult> { + const enriched = { ...data }; + const enrichedFields: string[] = []; + const warnings: string[] = []; + + debugLog('[Enrichment] 시작:', { + designCategoryCode: data.designCategoryCode, + materialGroupCode: data.materialGroupCode, + vendorCode: data.vendorCode, + contractSignerCode: data.contractSignerCode, + }); + + // 1. 설계기능코드 → 설계기능명 자동완성 + if (data.designCategoryCode && !data.designCategory) { + debugLog('[Enrichment] 설계기능명 조회 시도:', data.designCategoryCode); + const discipline = await getDisciplineCodeByCode(data.designCategoryCode); + if (discipline) { + enriched.designCategory = discipline.USR_DF_CHAR_18; + enrichedFields.push('designCategory'); + debugSuccess('[Enrichment] 설계기능명 자동완성:', { + code: data.designCategoryCode, + name: discipline.USR_DF_CHAR_18, + }); + } else { + debugWarn('[Enrichment] 설계기능코드를 찾을 수 없음:', data.designCategoryCode); + warnings.push( + `설계기능코드 '${data.designCategoryCode}'에 해당하는 설계기능명을 찾을 수 없습니다.` + ); + } + } + + // 2. 자재그룹코드 → 자재그룹명 자동완성 + if (data.materialGroupCode && !data.materialGroupName) { + debugLog('[Enrichment] 자재그룹명 조회 시도:', data.materialGroupCode); + const materialGroup = await getMaterialGroupByCode(data.materialGroupCode); + if (materialGroup) { + enriched.materialGroupName = materialGroup.materialGroupDescription; + enrichedFields.push('materialGroupName'); + debugSuccess('[Enrichment] 자재그룹명 자동완성:', { + code: data.materialGroupCode, + name: materialGroup.materialGroupDescription, + }); + } else { + debugWarn('[Enrichment] 자재그룹코드를 찾을 수 없음:', data.materialGroupCode); + warnings.push( + `자재그룹코드 '${data.materialGroupCode}'에 해당하는 자재그룹명을 찾을 수 없습니다.` + ); + } + } + + // 3. 협력업체코드 → 협력업체명 자동완성 + if (data.vendorCode && !data.vendorName) { + debugLog('[Enrichment] 협력업체명 조회 시도:', data.vendorCode); + const vendor = await getVendorByCode(data.vendorCode); + if (vendor) { + enriched.vendorName = vendor.vendorName; + enrichedFields.push('vendorName'); + debugSuccess('[Enrichment] 협력업체명 자동완성:', { + code: data.vendorCode, + name: vendor.vendorName, + }); + } else { + debugWarn('[Enrichment] 협력업체코드를 찾을 수 없음:', data.vendorCode); + warnings.push( + `협력업체코드 '${data.vendorCode}'에 해당하는 협력업체명을 찾을 수 없습니다.` + ); + } + } + + // 4. 계약서명주체코드 → 계약서명주체명 자동완성 + if (data.contractSignerCode && !data.contractSignerName) { + debugLog('[Enrichment] 계약서명주체명 조회 시도:', data.contractSignerCode); + const contractSigner = await getVendorByCode(data.contractSignerCode); + if (contractSigner) { + enriched.contractSignerName = contractSigner.vendorName; + enrichedFields.push('contractSignerName'); + debugSuccess('[Enrichment] 계약서명주체명 자동완성:', { + code: data.contractSignerCode, + name: contractSigner.vendorName, + }); + } else { + debugWarn('[Enrichment] 계약서명주체코드를 찾을 수 없음:', data.contractSignerCode); + warnings.push( + `계약서명주체코드 '${data.contractSignerCode}'에 해당하는 계약서명주체명을 찾을 수 없습니다.` + ); + } + } + + debugSuccess('[Enrichment] 완료:', { + enrichedFieldsCount: enrichedFields.length, + warningsCount: warnings.length, + }); + + return { + enriched, + enrichedFields, + warnings, + }; +} + diff --git a/lib/vendor-pool/excel-utils.ts b/lib/vendor-pool/excel-utils.ts index c1c2ac0a..81426ebd 100644 --- a/lib/vendor-pool/excel-utils.ts +++ b/lib/vendor-pool/excel-utils.ts @@ -17,25 +17,24 @@ export interface ExcelColumnConfig { export const vendorPoolExcelColumns: ExcelColumnConfig[] = [ { accessorKey: 'constructionSector', header: '조선/해양', width: 15, required: true }, { accessorKey: 'htDivision', header: 'H/T구분', width: 15, required: true }, - { accessorKey: 'designCategoryCode', header: '설계기능코드', width: 20 }, - { accessorKey: 'designCategory', header: '설계기능(공종)', width: 25, required: true }, + { accessorKey: 'designCategoryCode', header: '설계기능코드', width: 20, required: true }, + { accessorKey: 'designCategory', header: '설계기능(공종)', width: 25 }, // 코드로 자동완성 가능 { accessorKey: 'equipBulkDivision', header: 'Equip/Bulk', width: 15 }, { accessorKey: 'packageCode', header: '패키지코드', width: 20 }, { accessorKey: 'packageName', header: '패키지명', width: 25 }, { accessorKey: 'materialGroupCode', header: '자재그룹코드', width: 20 }, - { accessorKey: 'materialGroupName', header: '자재그룹명', width: 30, required: true }, + { accessorKey: 'materialGroupName', header: '자재그룹명', width: 30 }, // 코드로 자동완성 가능 { accessorKey: 'smCode', header: 'SM Code', width: 15 }, { accessorKey: 'similarMaterialNamePurchase', header: '유사자재명(구매)', width: 25 }, { accessorKey: 'similarMaterialNameOther', header: '유사자재명(기타)', width: 25 }, { accessorKey: 'vendorCode', header: '협력업체코드', width: 20 }, - { accessorKey: 'vendorName', header: '협력업체명', width: 25, required: true }, - { accessorKey: 'taxId', header: '사업자번호', width: 20, required: true }, + { accessorKey: 'vendorName', header: '협력업체명', width: 25 }, // 코드로 자동완성 가능 { accessorKey: 'faTarget', header: 'FA대상', width: 15, type: 'boolean' }, { accessorKey: 'faStatus', header: 'FA현황', width: 15 }, { accessorKey: 'tier', header: '등급', width: 15, required: true }, { accessorKey: 'isAgent', header: 'Agent여부', width: 15, type: 'boolean' }, { accessorKey: 'contractSignerCode', header: '계약서명주체코드', width: 20 }, - { accessorKey: 'contractSignerName', header: '계약서명주체명', width: 25, required: true }, + { accessorKey: 'contractSignerName', header: '계약서명주체명', width: 25 }, // 코드로 자동완성 가능 { accessorKey: 'headquarterLocation', header: '본사위치', width: 20, required: true }, { accessorKey: 'manufacturingLocation', header: '제작/선적지', width: 20, required: true }, { accessorKey: 'avlVendorName', header: 'AVL등재업체명', width: 25, required: true }, @@ -151,33 +150,29 @@ export async function createVendorPoolTemplate(filename?: string) { // 가이드 내용 const guideContent = [ '', - '■ 필수 입력 필드 (엑셀 헤더가 빨간색으로 표시됨)', - ' - 조선/해양: "조선" 또는 "해양"', - ' - H/T구분: "H", "T", 또는 "공통"', - ' - 설계기능(공종): 설계기능 한글명 (예: 전장, 기관)', - ' - 자재그룹명: 해당 자재그룹 정보', - ' - 협력업체명: 업체 한글명', - ' - 사업자번호: 업체 사업자등록번호', - ' - 등급: "Tier 1", "Tier 2", "등급 외"', - ' - 계약서명주체명: 계약 주체 업체명', - ' - 본사위치: 국가명을 입력', - ' - 제작/선적지: 입력', - ' - AVL등재업체명: AVL에 등재된 업체명', + '■ 필수 입력 필드 (헤더가 빨간색)', + ' - 조선/해양, H/T구분, 설계기능코드', + ' - 등급, 본사위치, 제작/선적지, AVL등재업체명', + '', + '■ 자동완성 기능 (코드 입력 시)', + ' 1. 코드가 있는 경우 → 코드만 입력하면 명칭 자동완성', + ' • 설계기능코드 → 설계기능명', + ' • 자재그룹코드 → 자재그룹명', + ' • 협력업체코드 → 협력업체명', + ' • 계약서명주체코드 → 계약서명주체명', '', - '■ Boolean (참/거짓) 필드 입력법', - ' - TRUE, true, 1, Y, y, O, o → 참', - ' - FALSE, false, 0, N, n → 거짓', - ' - 빈 값은 기본적으로 거짓(false)으로 처리', + ' 2. 코드가 없는 경우 → 명칭 직접 입력', '', - '■ 주의사항', - ' - 첫 번째 행의 안내 텍스트는 삭제하지 마세요', - ' - 헤더 행(2번째 행)은 수정하지 마세요', - ' - 데이터는 3번째 행부터 입력하세요', + '■ Boolean 필드', + ' - TRUE/true/1/Y/O → 참', + ' - FALSE/false/0/N 또는 빈 값 → 거짓', '', - '■ 문제 해결', - ' - 필드 길이 초과 오류: 해당 필드의 글자 수를 확인하세요', - ' - 필수 필드 누락: 빨간색 * 표시 필드를 모두 입력했는지 확인하세요', - ' - Boolean 값 오류: TRUE/FALSE 형태로 입력했는지 확인하세요' + '■ 입력 규칙', + ' - 조선/해양: "조선" 또는 "해양"', + ' - H/T구분: "H", "T", "공통"', + ' - 설계기능코드: 2자리 이하', + ' - Equip/Bulk: "E", "B", "S" (1자리)', + ' - 등급: "Tier 1", "Tier 2", "등급 외"' ] guideContent.forEach((content, index) => { diff --git a/lib/vendor-pool/service.ts b/lib/vendor-pool/service.ts index 97a0bcee..df947965 100644 --- a/lib/vendor-pool/service.ts +++ b/lib/vendor-pool/service.ts @@ -117,13 +117,6 @@ const _getVendorPools = async (input: GetVendorPoolSchema) => { condition = eq(vendorPool.vendorName, filter.value as string); } break; - case 'taxId': - if (filter.operator === 'iLike') { - condition = ilike(vendorPool.taxId, `%${filter.value}%`); - } else if (filter.operator === 'eq') { - condition = eq(vendorPool.taxId, filter.value as string); - } - break; case 'faStatus': if (filter.operator === 'iLike') { condition = ilike(vendorPool.faStatus, `%${filter.value}%`); @@ -341,7 +334,6 @@ const _getVendorPools = async (input: GetVendorPoolSchema) => { similarMaterialNameOther: item.similarMaterialNameOther || '', vendorCode: item.vendorCode || '', vendorName: item.vendorName || '', - taxId: item.taxId || '', faStatus: item.faStatus || '', faRemark: item.faRemark || '', tier: item.tier || '', @@ -444,7 +436,6 @@ export async function getVendorPoolById(id: number): Promise<VendorPool | null> similarMaterialNameOther: item.similarMaterialNameOther || '', vendorCode: item.vendorCode || '', vendorName: item.vendorName || '', - taxId: item.taxId || '', faStatus: item.faStatus || '', faRemark: item.faRemark || '', tier: item.tier || '', @@ -610,7 +601,6 @@ export async function createVendorPool(data: Omit<VendorPool, 'id' | 'registrati vendorName: data.vendorName, // 사업 및 인증 정보 - taxId: data.taxId, faTarget: data.faTarget ?? false, faStatus: data.faStatus, faRemark: data.faRemark, @@ -823,7 +813,6 @@ export async function updateVendorPool(id: number, data: Partial<VendorPool>): P if (data.vendorName !== undefined) updateData.vendorName = data.vendorName; // 사업 및 인증 정보 - if (data.taxId !== undefined) updateData.taxId = data.taxId; if (data.faTarget !== undefined) updateData.faTarget = data.faTarget; if (data.faStatus !== undefined) updateData.faStatus = data.faStatus; if (data.faRemark !== undefined) updateData.faRemark = data.faRemark; diff --git a/lib/vendor-pool/table/import-progress-dialog.tsx b/lib/vendor-pool/table/import-progress-dialog.tsx new file mode 100644 index 00000000..6fd20e36 --- /dev/null +++ b/lib/vendor-pool/table/import-progress-dialog.tsx @@ -0,0 +1,54 @@ +"use client" + +import React from 'react' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Progress } from "@/components/ui/progress" +import { Loader2 } from "lucide-react" + +interface ImportProgressDialogProps { + open: boolean + totalRows: number + processedRows: number +} + +export function ImportProgressDialog({ open, totalRows, processedRows }: ImportProgressDialogProps) { + const percentage = totalRows > 0 ? Math.round((processedRows / totalRows) * 100) : 0 + + return ( + <Dialog open={open} onOpenChange={() => {}}> + <DialogContent className="sm:max-w-md pointer-events-none [&>button]:hidden"> + <DialogHeader> + <DialogTitle className="flex items-center gap-2"> + <Loader2 className="h-5 w-5 animate-spin" /> + 데이터 Import 중... + </DialogTitle> + <DialogDescription> + 잠시만 기다려주세요. 데이터를 처리하고 있습니다. + </DialogDescription> + </DialogHeader> + + <div className="space-y-4 py-4"> + <div className="flex items-center justify-between text-sm"> + <span className="text-muted-foreground">진행 상황</span> + <span className="font-medium"> + {processedRows} / {totalRows}건 + </span> + </div> + + <Progress value={percentage} className="h-2" /> + + <div className="text-center text-sm text-muted-foreground"> + {percentage}% 완료 + </div> + </div> + </DialogContent> + </Dialog> + ) +} + diff --git a/lib/vendor-pool/table/import-result-dialog.tsx b/lib/vendor-pool/table/import-result-dialog.tsx new file mode 100644 index 00000000..2e541271 --- /dev/null +++ b/lib/vendor-pool/table/import-result-dialog.tsx @@ -0,0 +1,167 @@ +"use client" + +import React from 'react' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { ScrollArea } from "@/components/ui/scroll-area" +import { CheckCircle2, XCircle, AlertCircle, RefreshCw, FileText } from "lucide-react" +import { Separator } from "@/components/ui/separator" +import { Badge } from "@/components/ui/badge" + +export interface ImportResultItem { + rowNumber: number + status: 'success' | 'error' | 'duplicate' | 'warning' + message: string + data?: { + vendorName?: string + materialGroupName?: string + designCategory?: string + } +} + +export interface ImportResult { + totalRows: number + successCount: number + errorCount: number + duplicateCount: number + items: ImportResultItem[] // 실패한 건만 포함 +} + +interface ImportResultDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + result: ImportResult | null +} + +export function ImportResultDialog({ open, onOpenChange, result }: ImportResultDialogProps) { + if (!result) return null + + const getStatusIcon = (status: ImportResultItem['status']) => { + switch (status) { + case 'success': + return <CheckCircle2 className="h-4 w-4 text-green-600" /> + case 'error': + return <XCircle className="h-4 w-4 text-red-600" /> + case 'duplicate': + return <RefreshCw className="h-4 w-4 text-yellow-600" /> + case 'warning': + return <AlertCircle className="h-4 w-4 text-orange-600" /> + } + } + + const getStatusBadge = (status: ImportResultItem['status']) => { + switch (status) { + case 'success': + return <Badge variant="default" className="bg-green-600">성공</Badge> + case 'error': + return <Badge variant="destructive">실패</Badge> + case 'duplicate': + return <Badge variant="secondary" className="bg-yellow-600 text-white">중복</Badge> + case 'warning': + return <Badge variant="secondary" className="bg-orange-600 text-white">경고</Badge> + } + } + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-3xl max-h-[80vh]"> + <DialogHeader> + <DialogTitle className="flex items-center gap-2"> + <FileText className="h-5 w-5" /> + 엑셀 Import 결과 + </DialogTitle> + <DialogDescription> + 총 {result.totalRows}건의 데이터를 처리했습니다. + </DialogDescription> + </DialogHeader> + + {/* 요약 정보 */} + <div className="grid grid-cols-3 gap-4 py-4"> + <div className="flex flex-col items-center p-4 bg-green-50 rounded-lg border border-green-200"> + <CheckCircle2 className="h-8 w-8 text-green-600 mb-2" /> + <div className="text-2xl font-bold text-green-700">{result.successCount}</div> + <div className="text-sm text-green-600">성공</div> + </div> + + <div className="flex flex-col items-center p-4 bg-yellow-50 rounded-lg border border-yellow-200"> + <RefreshCw className="h-8 w-8 text-yellow-600 mb-2" /> + <div className="text-2xl font-bold text-yellow-700">{result.duplicateCount}</div> + <div className="text-sm text-yellow-600">중복</div> + </div> + + <div className="flex flex-col items-center p-4 bg-red-50 rounded-lg border border-red-200"> + <XCircle className="h-8 w-8 text-red-600 mb-2" /> + <div className="text-2xl font-bold text-red-700">{result.errorCount}</div> + <div className="text-sm text-red-600">실패</div> + </div> + </div> + + {/* 상세 정보 - 실패한 건만 표시 */} + {result.errorCount > 0 && ( + <> + <Separator /> + <div className="space-y-2"> + <h4 className="text-sm font-semibold text-red-600">실패한 항목 ({result.errorCount}건)</h4> + <ScrollArea className="h-[300px] rounded-md border p-4"> + <div className="space-y-3"> + {result.items.map((item, index) => ( + <div + key={index} + className="flex items-start gap-3 p-3 rounded-lg bg-red-50 border border-red-200 hover:bg-red-100 transition-colors" + > + <div className="flex-shrink-0 mt-0.5"> + {getStatusIcon(item.status)} + </div> + <div className="flex-1 min-w-0"> + <div className="flex items-center gap-2 mb-1"> + <span className="text-sm font-medium text-muted-foreground"> + 행 {item.rowNumber} + </span> + {getStatusBadge(item.status)} + </div> + <p className="text-sm text-foreground">{item.message}</p> + {item.data && ( + <div className="mt-2 text-xs text-muted-foreground space-y-1"> + {item.data.vendorName && ( + <div>• 협력업체: {item.data.vendorName}</div> + )} + {item.data.materialGroupName && ( + <div>• 자재그룹: {item.data.materialGroupName}</div> + )} + {item.data.designCategory && ( + <div>• 설계기능: {item.data.designCategory}</div> + )} + </div> + )} + </div> + </div> + ))} + + {result.items.length === 0 && ( + <div className="text-center text-muted-foreground py-8"> + 모든 항목이 정상적으로 처리되었습니다. + </div> + )} + </div> + </ScrollArea> + </div> + </> + )} + + {/* 하단 버튼 */} + <div className="flex justify-end gap-2"> + <Button variant="outline" onClick={() => onOpenChange(false)}> + 닫기 + </Button> + </div> + </DialogContent> + </Dialog> + ) +} + diff --git a/lib/vendor-pool/table/vendor-pool-excel-import-button.tsx b/lib/vendor-pool/table/vendor-pool-excel-import-button.tsx index e07987b3..cb39419d 100644 --- a/lib/vendor-pool/table/vendor-pool-excel-import-button.tsx +++ b/lib/vendor-pool/table/vendor-pool-excel-import-button.tsx @@ -19,6 +19,10 @@ import { vendorPoolExcelColumns } from '../excel-utils' import { decryptWithServerAction } from '@/components/drm/drmUtils' +import { debugLog, debugError, debugWarn, debugSuccess, debugProcess } from '@/lib/debug-utils' +import { enrichVendorPoolData } from '../enrichment-service' +import { ImportResultDialog, ImportResult, ImportResultItem } from './import-result-dialog' +import { ImportProgressDialog } from './import-progress-dialog' interface ImportExcelProps { onSuccess?: () => void @@ -28,40 +32,68 @@ export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) { const fileInputRef = useRef<HTMLInputElement>(null) const [isImporting, setIsImporting] = React.useState(false) const { data: session } = useSession() + const [importResult, setImportResult] = React.useState<ImportResult | null>(null) + const [showResultDialog, setShowResultDialog] = React.useState(false) + + // Progress 상태 + const [showProgressDialog, setShowProgressDialog] = React.useState(false) + const [totalRows, setTotalRows] = React.useState(0) + const [processedRows, setProcessedRows] = React.useState(0) // 헬퍼 함수들은 excel-utils에서 import const handleImport = async (event: React.ChangeEvent<HTMLInputElement>) => { const file = event.target.files?.[0] - if (!file) return + if (!file) { + debugWarn('[Import] 파일이 선택되지 않았습니다.') + return + } + + debugLog('[Import] 파일 임포트 시작:', { + fileName: file.name, + fileSize: file.size, + fileType: file.type + }) setIsImporting(true) try { // DRM 복호화 처리 + debugProcess('[Import] DRM 복호화 시작') toast.info("파일을 복호화하고 있습니다...") let decryptedData: ArrayBuffer try { decryptedData = await decryptWithServerAction(file) + debugSuccess('[Import] DRM 복호화 성공') toast.success("파일 복호화가 완료되었습니다.") } catch (drmError) { + debugWarn('[Import] DRM 복호화 실패, 원본 파일로 진행:', drmError) console.warn("DRM 복호화 실패, 원본 파일로 진행합니다:", drmError) toast.warning("DRM 복호화에 실패했습니다. 원본 파일로 진행합니다.") decryptedData = await file.arrayBuffer() + debugLog('[Import] 원본 파일 ArrayBuffer 로드 완료, size:', decryptedData.byteLength) } // 복호화된 데이터로 ExcelJS 워크북 로드 + debugProcess('[Import] ExcelJS 워크북 로드 시작') toast.info("엑셀 파일을 분석하고 있습니다...") const workbook = new ExcelJS.Workbook() await workbook.xlsx.load(decryptedData) + debugSuccess('[Import] ExcelJS 워크북 로드 완료') // Get the first worksheet const worksheet = workbook.getWorksheet(1) if (!worksheet) { + debugError('[Import] 워크시트를 찾을 수 없습니다.') toast.error("No worksheet found in the spreadsheet") return } + debugLog('[Import] 워크시트 확인 완료:', { + name: worksheet.name, + rowCount: worksheet.rowCount, + columnCount: worksheet.columnCount + }) // Check if there's an instruction row (템플릿 안내 텍스트가 있는지 확인) const firstRowText = getCellValueAsString(worksheet.getRow(1).getCell(1)); @@ -71,10 +103,17 @@ export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) { (worksheet.getRow(1).getCell(1).value !== null && worksheet.getRow(1).getCell(2).value === null); + debugLog('[Import] 첫 번째 행 확인:', { + firstRowText, + hasInstructionRow + }) + // Get header row index (row 2 if there's an instruction row, otherwise row 1) const headerRowIndex = hasInstructionRow ? 2 : 1; + debugLog('[Import] 헤더 행 인덱스:', headerRowIndex) // Get column headers and their indices + debugProcess('[Import] 컬럼 헤더 매핑 시작') const headerRow = worksheet.getRow(headerRowIndex); const columnIndices: Record<string, number> = {}; @@ -87,15 +126,25 @@ export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) { } }); + debugLog('[Import] 컬럼 매핑 완료:', { + mappedColumns: Object.keys(columnIndices).length, + columnIndices + }) + // Process data rows + debugProcess('[Import] 데이터 행 처리 시작') const rows: any[] = []; const startRow = headerRowIndex + 1; + let skippedRows = 0; for (let i = startRow; i <= worksheet.rowCount; i++) { const row = worksheet.getRow(i); // Skip empty rows - if (row.cellCount === 0) continue; + if (row.cellCount === 0) { + skippedRows++; + continue; + } // Check if this is likely an empty template row (빈 템플릿 행 건너뛰기) let hasAnyData = false; @@ -105,7 +154,10 @@ export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) { break; } } - if (!hasAnyData) continue; + if (!hasAnyData) { + skippedRows++; + continue; + } const rowData: Record<string, any> = {}; let hasData = false; @@ -121,26 +173,44 @@ export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) { if (hasData) { rows.push(rowData); + } else { + skippedRows++; } } + debugLog('[Import] 데이터 행 처리 완료:', { + totalRows: worksheet.rowCount - headerRowIndex, + validRows: rows.length, + skippedRows + }) + if (rows.length === 0) { + debugWarn('[Import] 유효한 데이터가 없습니다.') toast.error("No data found in the spreadsheet") setIsImporting(false) return } + // Progress Dialog 표시 + setTotalRows(rows.length) + setProcessedRows(0) + setShowProgressDialog(true) + // Process each row + debugProcess('[Import] 데이터베이스 저장 시작') let successCount = 0; let errorCount = 0; - let duplicateErrors: string[] = []; + let duplicateCount = 0; + const resultItems: ImportResultItem[] = []; // 실패한 건만 포함 // Create promises for all vendor pool creation operations - const promises = rows.map(async (row) => { + const promises = rows.map(async (row, rowIndex) => { // Excel 컬럼 설정을 기반으로 데이터 매핑 (catch 블록에서도 사용하기 위해 밖에서 선언) const vendorPoolData: any = {}; try { + debugLog(`[Import] 행 ${rowIndex + 1}/${rows.length} 처리 시작 - 원본 데이터:`, row) + vendorPoolExcelColumns.forEach(column => { const { accessorKey, type } = column; const value = row[accessorKey] || ''; @@ -159,16 +229,112 @@ export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) { vendorPoolData.registrant = session?.user?.name || 'system'; vendorPoolData.lastModifier = session?.user?.name || 'system'; - // Validate required fields - if (!vendorPoolData.constructionSector || !vendorPoolData.htDivision || - !vendorPoolData.designCategory || !vendorPoolData.vendorName || - !vendorPoolData.designCategoryCode || !vendorPoolData.equipBulkDivision) { - console.error("Missing required fields", vendorPoolData); + debugLog(`[Import] 행 ${rowIndex + 1} 데이터 매핑 완료 - 전체 필드:`, { + '공사부문': vendorPoolData.constructionSector, + 'H/T구분': vendorPoolData.htDivision, + '설계기능코드': vendorPoolData.designCategoryCode, + '설계기능': vendorPoolData.designCategory, + 'Equip/Bulk구분': vendorPoolData.equipBulkDivision, + '협력업체코드': vendorPoolData.vendorCode, + '협력업체명': vendorPoolData.vendorName, + '자재그룹코드': vendorPoolData.materialGroupCode, + '자재그룹명': vendorPoolData.materialGroupName, + '계약서명주체코드': vendorPoolData.contractSignerCode, + '계약서명주체명': vendorPoolData.contractSignerName, + '사업자번호': vendorPoolData.taxId, + '패키지코드': vendorPoolData.packageCode, + '패키지명': vendorPoolData.packageName + }) + + // 코드를 기반으로 자동완성 수행 + debugProcess(`[Import] 행 ${rowIndex + 1} enrichment 시작`); + const enrichmentResult = await enrichVendorPoolData({ + designCategoryCode: vendorPoolData.designCategoryCode, + designCategory: vendorPoolData.designCategory, + materialGroupCode: vendorPoolData.materialGroupCode, + materialGroupName: vendorPoolData.materialGroupName, + vendorCode: vendorPoolData.vendorCode, + vendorName: vendorPoolData.vendorName, + contractSignerCode: vendorPoolData.contractSignerCode, + contractSignerName: vendorPoolData.contractSignerName, + }); + + // enrichment 결과 적용 + if (enrichmentResult.enrichedFields.length > 0) { + debugSuccess(`[Import] 행 ${rowIndex + 1} enrichment 완료 - 자동완성된 필드: ${enrichmentResult.enrichedFields.join(', ')}`); + + // enrichment된 데이터를 vendorPoolData에 반영 + if (enrichmentResult.enriched.designCategory) { + vendorPoolData.designCategory = enrichmentResult.enriched.designCategory; + } + if (enrichmentResult.enriched.materialGroupName) { + vendorPoolData.materialGroupName = enrichmentResult.enriched.materialGroupName; + } + if (enrichmentResult.enriched.vendorName) { + vendorPoolData.vendorName = enrichmentResult.enriched.vendorName; + } + if (enrichmentResult.enriched.contractSignerName) { + vendorPoolData.contractSignerName = enrichmentResult.enriched.contractSignerName; + } + } + + // enrichment 경고 메시지 로깅만 (resultItems에 추가하지 않음) + if (enrichmentResult.warnings.length > 0) { + debugWarn(`[Import] 행 ${rowIndex + 1} enrichment 경고:`, enrichmentResult.warnings); + enrichmentResult.warnings.forEach(warning => { + console.warn(`Row ${rowIndex + 1}: ${warning}`); + }); + } + + // Validate required fields (필수 필드 검증) + // 자동완성 가능한 필드는 코드가 있으면 명칭은 optional로 처리 + const requiredFieldsCheck = { + constructionSector: { value: vendorPoolData.constructionSector, label: '공사부문' }, + htDivision: { value: vendorPoolData.htDivision, label: 'H/T구분' }, + designCategoryCode: { value: vendorPoolData.designCategoryCode, label: '설계기능코드' } + }; + + // 설계기능: 설계기능코드가 없으면 설계기능명 필수 + if (!vendorPoolData.designCategoryCode && !vendorPoolData.designCategory) { + requiredFieldsCheck['designCategory'] = { value: vendorPoolData.designCategory, label: '설계기능' }; + } + + // 협력업체명: 협력업체코드가 없으면 협력업체명 필수 + if (!vendorPoolData.vendorCode && !vendorPoolData.vendorName) { + requiredFieldsCheck['vendorName'] = { value: vendorPoolData.vendorName, label: '협력업체명' }; + } + + const missingRequiredFields = Object.entries(requiredFieldsCheck) + .filter(([_, field]) => !field.value) + .map(([key, field]) => `${field.label}(${key})`); + + if (missingRequiredFields.length > 0) { + debugError(`[Import] 행 ${rowIndex + 1} 필수 필드 누락 [${missingRequiredFields.length}개]:`, { + missingFields: missingRequiredFields, + currentData: vendorPoolData + }); + console.error(`Missing required fields in row ${rowIndex + 1}:`, missingRequiredFields.join(', ')); errorCount++; + resultItems.push({ + rowNumber: rowIndex + 1, + status: 'error', + message: `필수 필드 누락: ${missingRequiredFields.join(', ')}`, + data: { + vendorName: vendorPoolData.vendorName, + materialGroupName: vendorPoolData.materialGroupName, + designCategory: vendorPoolData.designCategory, + } + }); + + // Progress 업데이트 + setProcessedRows(prev => prev + 1); + return null; } + + debugSuccess(`[Import] 행 ${rowIndex + 1} 필수 필드 검증 통과`); - // Validate field lengths and formats + // Validate field lengths and formats (필드 길이 및 형식 검증) const validationErrors: string[] = []; if (vendorPoolData.designCategoryCode && vendorPoolData.designCategoryCode.length > 2) { @@ -188,72 +354,174 @@ export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) { } if (validationErrors.length > 0) { - console.error("Validation errors:", validationErrors, vendorPoolData); + debugError(`[Import] 행 ${rowIndex + 1} 검증 실패 [${validationErrors.length}개 오류]:`, { + errors: validationErrors, + problematicFields: { + designCategoryCode: vendorPoolData.designCategoryCode, + equipBulkDivision: vendorPoolData.equipBulkDivision, + constructionSector: vendorPoolData.constructionSector, + htDivision: vendorPoolData.htDivision + } + }); + console.error(`Validation errors in row ${rowIndex + 1}:`, validationErrors.join(' | ')); errorCount++; + resultItems.push({ + rowNumber: rowIndex + 1, + status: 'error', + message: `검증 실패: ${validationErrors.join(', ')}`, + data: { + vendorName: vendorPoolData.vendorName, + materialGroupName: vendorPoolData.materialGroupName, + designCategory: vendorPoolData.designCategory, + } + }); + + // Progress 업데이트 + setProcessedRows(prev => prev + 1); + return null; } + + debugSuccess(`[Import] 행 ${rowIndex + 1} 형식 검증 통과`); if (!session || !session.user || !session.user.id) { + debugError(`[Import] 행 ${rowIndex + 1} 세션 오류: 로그인 정보 없음`); toast.error("인증 오류. 로그인 정보를 찾을 수 없습니다.") return } // Create the vendor pool entry + debugProcess(`[Import] 행 ${rowIndex + 1} 데이터베이스 저장 시도`); const result = await createVendorPool(vendorPoolData as any) if (!result) { + debugError(`[Import] 행 ${rowIndex + 1} 저장 실패: createVendorPool returned null`); console.error(`Failed to import row - createVendorPool returned null:`, vendorPoolData); errorCount++; + resultItems.push({ + rowNumber: rowIndex + 1, + status: 'error', + message: '데이터 저장 실패', + data: { + vendorName: vendorPoolData.vendorName, + materialGroupName: vendorPoolData.materialGroupName, + designCategory: vendorPoolData.designCategory, + } + }); + + // Progress 업데이트 + setProcessedRows(prev => prev + 1); + return null; } + debugSuccess(`[Import] 행 ${rowIndex + 1} 저장 성공 (ID: ${result.id})`); successCount++; + // 성공한 건은 resultItems에 추가하지 않음 + + // Progress 업데이트 + setProcessedRows(prev => prev + 1); + return result; } catch (error) { + debugError(`[Import] 행 ${rowIndex + 1} 처리 중 예외 발생:`, error); console.error("Error processing row:", error, row); // Unique 제약 조건 위반 감지 (중복 데이터) const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage === 'DUPLICATE_VENDOR_POOL') { - duplicateErrors.push(`공사부문(${vendorPoolData.constructionSector}), H/T(${vendorPoolData.htDivision}), 자재그룹코드(${vendorPoolData.materialGroupCode}), 협력업체명(${vendorPoolData.vendorName})`); - // 중복인 경우 에러 카운트를 증가시키지 않고 건너뜀 (전체 import 중단하지 않음) + debugWarn(`[Import] 행 ${rowIndex + 1} 중복 데이터 감지:`, { + constructionSector: vendorPoolData.constructionSector, + htDivision: vendorPoolData.htDivision, + materialGroupCode: vendorPoolData.materialGroupCode, + vendorName: vendorPoolData.vendorName + }); + duplicateCount++; + // 중복 건은 resultItems에 추가하지 않음 + + // Progress 업데이트 + setProcessedRows(prev => prev + 1); + return null; } // 다른 에러의 경우 에러 카운트 증가 errorCount++; + resultItems.push({ + rowNumber: rowIndex + 1, + status: 'error', + message: `처리 중 오류 발생: ${errorMessage}`, + data: { + vendorName: vendorPoolData.vendorName, + materialGroupName: vendorPoolData.materialGroupName, + designCategory: vendorPoolData.designCategory, + } + }); + + // Progress 업데이트 + setProcessedRows(prev => prev + 1); + return null; } }); // Wait for all operations to complete + debugProcess('[Import] 모든 Promise 완료 대기 중...') await Promise.all(promises); + debugSuccess('[Import] 모든 데이터 처리 완료') + + debugLog('[Import] 최종 결과:', { + totalRows: rows.length, + successCount, + errorCount, + duplicateCount + }) + + // Progress Dialog 닫기 + setShowProgressDialog(false) + + // Import 결과 Dialog 데이터 생성 (실패한 건만 포함) + const result: ImportResult = { + totalRows: rows.length, + successCount, + errorCount, + duplicateCount, + items: resultItems // 실패한 건만 포함됨 + } // Show results if (successCount > 0) { + debugSuccess(`[Import] 임포트 성공: ${successCount}개 항목`); toast.success(`${successCount}개 항목이 성공적으로 가져와졌습니다.`); if (errorCount > 0) { - toast.warning(`${errorCount}개 항목 가져오기에 실패했습니다.`); + debugWarn(`[Import] 일부 실패: ${errorCount}개 항목`); } // Call the success callback to refresh data onSuccess?.(); } else if (errorCount > 0) { + debugError(`[Import] 모든 항목 실패: ${errorCount}개`); toast.error(`모든 ${errorCount}개 항목 가져오기에 실패했습니다. 데이터 형식을 확인하세요.`); } - // 중복 데이터가 있었던 경우 개별적으로 표시 (성공/실패와 별개로 처리) - if (duplicateErrors.length > 0) { - duplicateErrors.forEach(errorMsg => { - toast.warning(`중복 데이터로 건너뜀: ${errorMsg}`); - }); + if (duplicateCount > 0) { + debugWarn(`[Import] 중복 데이터: ${duplicateCount}개`); + toast.warning(`${duplicateCount}개의 중복 데이터가 감지되었습니다.`); } + // Import 결과 Dialog 표시 + setImportResult(result); + setShowResultDialog(true); + } catch (error) { + debugError('[Import] 전체 임포트 프로세스 실패:', error); console.error("Import error:", error); toast.error("Error importing data. Please check file format."); + setShowProgressDialog(false); } finally { + debugLog('[Import] 임포트 프로세스 종료'); setIsImporting(false); + setShowProgressDialog(false); // Reset the file input if (fileInputRef.current) { fileInputRef.current.value = ''; @@ -286,6 +554,20 @@ export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) { {isImporting ? "Importing..." : "Import"} </span> </Button> + + {/* Import Progress Dialog */} + <ImportProgressDialog + open={showProgressDialog} + totalRows={totalRows} + processedRows={processedRows} + /> + + {/* Import 결과 Dialog */} + <ImportResultDialog + open={showResultDialog} + onOpenChange={setShowResultDialog} + result={importResult} + /> </> ) } diff --git a/lib/vendor-pool/table/vendor-pool-table-columns.tsx b/lib/vendor-pool/table/vendor-pool-table-columns.tsx index 897b8b1e..5676250b 100644 --- a/lib/vendor-pool/table/vendor-pool-table-columns.tsx +++ b/lib/vendor-pool/table/vendor-pool-table-columns.tsx @@ -19,7 +19,6 @@ interface VendorPoolTableMeta { onSaveEmptyRow?: (tempId: string) => Promise<void> onCancelEmptyRow?: (tempId: string) => void isEmptyRow?: (id: string) => boolean - onTaxIdChange?: (id: string, taxId: string) => Promise<void> } // Vendor Pool 데이터 타입 - 스키마 기반 + 테이블용 추가 필드 @@ -430,6 +429,7 @@ export const columns: ColumnDef<VendorPoolItem>[] = [ id: 0, // 실제로는 vendorId가 있어야 하지만 여기서는 임시로 0 사용 vendorName, vendorCode: vendorCode || null, + taxId: null, // 사업자번호는 vendor-pool에서 관리하지 않음 status: "ACTIVE", // 임시 값 displayText: vendorName + (vendorCode ? ` (${vendorCode})` : "") } : null @@ -438,18 +438,16 @@ export const columns: ColumnDef<VendorPoolItem>[] = [ 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 || "") } } 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", "") } } } @@ -532,38 +530,6 @@ export const columns: ColumnDef<VendorPoolItem>[] = [ size: 130, }, { - accessorKey: "taxId", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">사업자번호 *</span>} /> - ), - 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) - } - } - - const onChange = async (newValue: any) => { - if (table.options.meta?.onTaxIdChange) { - await table.options.meta.onTaxIdChange(row.original.id, newValue) - } - } - - return ( - <EditableCell - value={value} - type="text" - onSave={onSave} - onChange={onChange} - placeholder="사업자번호 입력" - maxLength={50} - /> - ) - }, - size: 120, - }, - { accessorKey: "faTarget", header: "FA대상", cell: ({ row, table }) => { @@ -727,6 +693,7 @@ export const columns: ColumnDef<VendorPoolItem>[] = [ id: 0, // 실제로는 vendorId가 있어야 하지만 여기서는 임시로 0 사용 vendorName: contractSignerName, vendorCode: contractSignerCode || null, + taxId: null, // 사업자번호는 vendor-pool에서 관리하지 않음 status: "ACTIVE", // 임시 값 displayText: contractSignerName + (contractSignerCode ? ` (${contractSignerCode})` : "") } : null @@ -736,7 +703,6 @@ export const columns: ColumnDef<VendorPoolItem>[] = [ 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) diff --git a/lib/vendor-pool/table/vendor-pool-table.tsx b/lib/vendor-pool/table/vendor-pool-table.tsx index 2696b354..336c93f5 100644 --- a/lib/vendor-pool/table/vendor-pool-table.tsx +++ b/lib/vendor-pool/table/vendor-pool-table.tsx @@ -309,14 +309,13 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP const finalData = { ...rowData, ...changes } // 필수 필드 검증 (최종 데이터 기준) - const requiredFields = ['constructionSector', 'htDivision', 'designCategory', 'taxId', 'vendorName', 'materialGroupCode', 'materialGroupName', 'tier', 'contractSignerName', 'headquarterLocation', 'manufacturingLocation', 'avlVendorName'] + const requiredFields = ['constructionSector', 'htDivision', 'designCategory', 'vendorName', 'materialGroupCode', 'materialGroupName', 'tier', 'contractSignerName', 'headquarterLocation', 'manufacturingLocation', 'avlVendorName'] // 필드명과 한국어 레이블 매핑 const fieldLabels: Record<string, string> = { constructionSector: '공사부문', htDivision: 'H/T구분', designCategory: '설계기능', - taxId: '사업자번호', vendorName: '협력업체명', materialGroupCode: '자재그룹코드', materialGroupName: '자재그룹명', diff --git a/lib/vendor-pool/types.ts b/lib/vendor-pool/types.ts index c6501d59..8a6e9881 100644 --- a/lib/vendor-pool/types.ts +++ b/lib/vendor-pool/types.ts @@ -34,7 +34,6 @@ export interface VendorPool { vendorName: string // 사업 및 인증 정보 - taxId: string // 사업자번호 faTarget: boolean // FA대상 faStatus: string // FA현황 faRemark: string // FA상세 diff --git a/lib/vendor-pool/validations.ts b/lib/vendor-pool/validations.ts index 642ccc47..60294edb 100644 --- a/lib/vendor-pool/validations.ts +++ b/lib/vendor-pool/validations.ts @@ -44,7 +44,6 @@ export const vendorPoolSearchParamsCache = createSearchParamsCache({ // 협력업체 정보 vendorCode: parseAsString.withDefault(""), // 협력업체 코드 vendorName: parseAsString.withDefault(""), // 협력업체 명 - taxId: parseAsString.withDefault(""), // 사업자번호 // 인증/상태 정보 faStatus: parseAsString.withDefault(""), // FA현황 |
