summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-11-05 16:07:43 +0900
committerjoonhoekim <26rote@gmail.com>2025-11-05 16:07:43 +0900
commit208ed7ff11d0f822d3d243c5833d31973904349e (patch)
treeb1a15e3a7f8242294397f433a8484102c5743e69
parentad6bde0250cfe014d5f78747ec76ac59df95a25d (diff)
(김준회) vendor-pool: excel-import 코드기반검색처리, 템플릿 변경 (구매 김수진 프로 요청사항)
-rw-r--r--components/common/vendor/vendor-service.ts38
-rw-r--r--db/schema/avl/vendor-pool.ts2
-rw-r--r--lib/material/material-group-service.ts38
-rw-r--r--lib/vendor-pool/enrichment-service.ts140
-rw-r--r--lib/vendor-pool/excel-utils.ts55
-rw-r--r--lib/vendor-pool/service.ts11
-rw-r--r--lib/vendor-pool/table/import-progress-dialog.tsx54
-rw-r--r--lib/vendor-pool/table/import-result-dialog.tsx167
-rw-r--r--lib/vendor-pool/table/vendor-pool-excel-import-button.tsx322
-rw-r--r--lib/vendor-pool/table/vendor-pool-table-columns.tsx40
-rw-r--r--lib/vendor-pool/table/vendor-pool-table.tsx3
-rw-r--r--lib/vendor-pool/types.ts1
-rw-r--r--lib/vendor-pool/validations.ts1
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현황