diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-03 02:47:09 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-03 02:47:09 +0000 |
| commit | 6f22fc9ebc8d175041aa18cf0986592e57d03f63 (patch) | |
| tree | a1f511d42cf6eaeb18ab41a61374731166886ecd /lib/tech-vendor-possible-items/table | |
| parent | 78d76dd27148a8b74a99b4ee984fd800fd92d00d (diff) | |
(최겸) 기술영업 벤더별 아이템 조회 기능 추가
Diffstat (limited to 'lib/tech-vendor-possible-items/table')
6 files changed, 969 insertions, 0 deletions
diff --git a/lib/tech-vendor-possible-items/table/excel-export.tsx b/lib/tech-vendor-possible-items/table/excel-export.tsx new file mode 100644 index 00000000..d3c4dea5 --- /dev/null +++ b/lib/tech-vendor-possible-items/table/excel-export.tsx @@ -0,0 +1,181 @@ +import * as ExcelJS from 'exceljs';
+import { saveAs } from "file-saver";
+import type { TechVendorPossibleItemsData } from '../service';
+import { format } from 'date-fns';
+import { ko } from 'date-fns/locale';
+
+/**
+ * 기술영업 벤더 가능 아이템 데이터를 Excel 파일로 내보내기
+ */
+export async function exportTechVendorPossibleItemsToExcel(
+ data: TechVendorPossibleItemsData[]
+) {
+ try {
+ // 워크북 생성
+ const workbook = new ExcelJS.Workbook();
+ workbook.creator = 'Tech Vendor Possible Items Management System';
+ workbook.created = new Date();
+
+ // 워크시트 생성
+ const worksheet = workbook.addWorksheet('기술영업 벤더 가능 아이템');
+
+ // 컬럼 헤더 정의 및 스타일 적용
+ worksheet.columns = [
+ { header: '번호', key: 'id', width: 10 },
+ { header: '벤더코드', key: 'vendorCode', width: 15 },
+ { header: '벤더명', key: 'vendorName', width: 25 },
+ { header: '벤더타입', key: 'techVendorType', width: 20 },
+ { header: '아이템코드', key: 'itemCode', width: 20 },
+ { header: '생성일시', key: 'createdAt', width: 20 },
+ ];
+
+ // 헤더 스타일 적용
+ const headerRow = worksheet.getRow(1);
+ headerRow.eachCell((cell) => {
+ cell.fill = {
+ type: 'pattern',
+ pattern: 'solid',
+ fgColor: { argb: 'FFE6F3FF' }
+ };
+ cell.font = {
+ bold: true,
+ color: { argb: 'FF1F4E79' }
+ };
+ cell.border = {
+ top: { style: 'thin' },
+ left: { style: 'thin' },
+ bottom: { style: 'thin' },
+ right: { style: 'thin' }
+ };
+ cell.alignment = {
+ vertical: 'middle',
+ horizontal: 'center'
+ };
+ });
+
+ // 데이터 추가
+ data.forEach((item, index) => {
+ // 벤더 타입 파싱
+ let vendorTypes = '';
+ try {
+ const parsed = JSON.parse(item.techVendorType || "[]");
+ vendorTypes = Array.isArray(parsed) ? parsed.join(', ') : item.techVendorType;
+ } catch {
+ vendorTypes = item.techVendorType;
+ }
+
+ const row = worksheet.addRow({
+ id: item.id,
+ vendorCode: item.vendorCode || '-',
+ vendorName: item.vendorName,
+ techVendorType: vendorTypes,
+ itemCode: item.itemCode,
+ createdAt: format(item.createdAt, 'yyyy-MM-dd HH:mm', { locale: ko }),
+ });
+
+ // 데이터 행 스타일
+ row.eachCell((cell, colNumber) => {
+ cell.border = {
+ top: { style: 'thin' },
+ left: { style: 'thin' },
+ bottom: { style: 'thin' },
+ right: { style: 'thin' }
+ };
+
+ if (colNumber === 1) {
+ // ID 컬럼 가운데 정렬
+ cell.alignment = { vertical: 'middle', horizontal: 'center' };
+ } else {
+ // 나머지 컬럼 왼쪽 정렬
+ cell.alignment = { vertical: 'middle', horizontal: 'left' };
+ }
+ });
+
+ // 홀수 행 배경색
+ if (index % 2 === 1) {
+ row.eachCell((cell) => {
+ cell.fill = {
+ type: 'pattern',
+ pattern: 'solid',
+ fgColor: { argb: 'FFF8F9FA' }
+ };
+ });
+ }
+ });
+
+ // 요약 정보 워크시트 생성
+ const summarySheet = workbook.addWorksheet('요약 정보');
+
+ const summaryData = [
+ ['기술영업 벤더 가능 아이템 현황', ''],
+ ['', ''],
+ ['총 항목 수:', data.length.toLocaleString()],
+ ['고유 벤더 수:', new Set(data.map(item => item.vendorId)).size.toLocaleString()],
+ ['고유 아이템 수:', new Set(data.map(item => item.itemCode)).size.toLocaleString()],
+ ['', ''],
+ ['벤더 타입별 분포:', ''],
+ ...getVendorTypeDistribution(data),
+ ['', ''],
+ ['내보내기 일시:', format(new Date(), 'yyyy-MM-dd HH:mm:ss', { locale: ko })],
+ ];
+
+ summaryData.forEach((rowData, index) => {
+ const row = summarySheet.addRow(rowData);
+ if (index === 0) {
+ // 제목 스타일
+ row.getCell(1).font = { bold: true, size: 16, color: { argb: 'FF1F4E79' } };
+ } else if (typeof rowData[0] === 'string' && rowData[0].includes(':') && rowData[1] === '') {
+ // 섹션 제목 스타일
+ row.getCell(1).font = { bold: true, color: { argb: 'FF1F4E79' } };
+ }
+ });
+
+ summarySheet.getColumn(1).width = 30;
+ summarySheet.getColumn(2).width = 20;
+
+ // 파일 생성 및 다운로드
+ const buffer = await workbook.xlsx.writeBuffer();
+ const blob = new Blob([buffer], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+ });
+
+ const fileName = `기술영업_벤더_가능_아이템_${format(new Date(), 'yyyyMMdd_HHmmss')}.xlsx`;
+ saveAs(blob, fileName);
+
+ return { success: true };
+ } catch (error) {
+ console.error("Excel 내보내기 중 오류:", error);
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "내보내기 중 오류가 발생했습니다."
+ };
+ }
+}
+
+/**
+ * 벤더 타입별 분포 계산
+ */
+function getVendorTypeDistribution(data: TechVendorPossibleItemsData[]): [string, string][] {
+ const typeCount = new Map<string, number>();
+
+ data.forEach(item => {
+ try {
+ const parsed = JSON.parse(item.techVendorType || "[]");
+ const types = Array.isArray(parsed) ? parsed : [item.techVendorType];
+
+ types.forEach(type => {
+ if (type) {
+ typeCount.set(type, (typeCount.get(type) || 0) + 1);
+ }
+ });
+ } catch {
+ if (item.techVendorType) {
+ typeCount.set(item.techVendorType, (typeCount.get(item.techVendorType) || 0) + 1);
+ }
+ }
+ });
+
+ return Array.from(typeCount.entries())
+ .sort((a, b) => b[1] - a[1])
+ .map(([type, count]) => [` - ${type}`, count.toLocaleString()]);
+}
\ No newline at end of file diff --git a/lib/tech-vendor-possible-items/table/excel-import.tsx b/lib/tech-vendor-possible-items/table/excel-import.tsx new file mode 100644 index 00000000..fbf984dd --- /dev/null +++ b/lib/tech-vendor-possible-items/table/excel-import.tsx @@ -0,0 +1,220 @@ +"use client";
+
+import * as ExcelJS from 'exceljs';
+import { ImportTechVendorPossibleItemData, ImportResult, importTechVendorPossibleItems } from '../service';
+import { saveAs } from "file-saver";
+
+export interface ExcelImportResult extends ImportResult {
+ errorFileUrl?: string;
+}
+
+/**
+ * Excel 파일에서 tech vendor possible items 데이터를 읽고 import
+ */
+export async function importTechVendorPossibleItemsFromExcel(
+ file: File
+): Promise<ExcelImportResult> {
+ try {
+ const buffer = await file.arrayBuffer();
+ const workbook = new ExcelJS.Workbook();
+ await workbook.xlsx.load(buffer);
+
+ // 첫 번째 워크시트에서 데이터 읽기
+ const worksheet = workbook.getWorksheet(1);
+ if (!worksheet) {
+ return {
+ success: false,
+ totalRows: 0,
+ successCount: 0,
+ failedRows: [{ row: 0, error: "워크시트를 찾을 수 없습니다." }],
+ };
+ }
+
+ const data: ImportTechVendorPossibleItemData[] = [];
+
+ // 데이터 행 읽기 (헤더 제외)
+ worksheet.eachRow((row, rowNumber) => {
+ if (rowNumber === 1) return; // 헤더 건너뛰기
+
+ const itemCode = row.getCell(1).value?.toString()?.trim();
+ const vendorCode = row.getCell(2).value?.toString()?.trim();
+ const vendorEmail = row.getCell(3).value?.toString()?.trim();
+
+ // 빈 행 건너뛰기
+ if (!itemCode && !vendorCode && !vendorEmail) return;
+
+ // 벤더 코드 또는 이메일 중 하나는 있어야 함
+ if (itemCode && (vendorCode || vendorEmail)) {
+ data.push({
+ vendorCode: vendorCode || '',
+ vendorEmail: vendorEmail || '',
+ itemCode,
+ });
+ } else {
+ // 불완전한 데이터 처리
+ data.push({
+ vendorCode: vendorCode || '',
+ vendorEmail: vendorEmail || '',
+ itemCode: itemCode || '',
+ });
+ }
+ });
+
+ if (data.length === 0) {
+ return {
+ success: false,
+ totalRows: 0,
+ successCount: 0,
+ failedRows: [{ row: 0, error: "가져올 데이터가 없습니다. 템플릿 형식을 확인하세요." }],
+ };
+ }
+
+ // 서비스를 통해 import 실행
+ const result = await importTechVendorPossibleItems(data);
+
+ // 실패한 항목이 있으면 오류 파일 생성
+ if (result.failedRows.length > 0) {
+ const errorFileUrl = await createErrorExcelFile(result.failedRows);
+ return {
+ ...result,
+ errorFileUrl,
+ };
+ }
+
+ return result;
+ } catch (error) {
+ console.error("Excel import 중 오류:", error);
+ return {
+ success: false,
+ totalRows: 0,
+ successCount: 0,
+ failedRows: [
+ {
+ row: 0,
+ error: error instanceof Error ? error.message : "파일 처리 중 오류가 발생했습니다. 파일 형식을 확인하세요.",
+ },
+ ],
+ };
+ }
+}
+
+/**
+ * 실패한 항목들을 포함한 오류 Excel 파일 생성
+ */
+async function createErrorExcelFile(
+ failedRows: ImportResult['failedRows']
+): Promise<string> {
+ try {
+ const workbook = new ExcelJS.Workbook();
+ const worksheet = workbook.addWorksheet('Import 오류 목록');
+
+ // 헤더 설정
+ worksheet.columns = [
+ { header: '행 번호', key: 'row', width: 10 },
+ { header: '아이템코드', key: 'itemCode', width: 20 },
+ { header: '벤더코드', key: 'vendorCode', width: 15 },
+ { header: '벤더이메일', key: 'vendorEmail', width: 30 },
+ { header: '오류 내용', key: 'error', width: 60 },
+ { header: '해결 방법', key: 'solution', width: 40 },
+ ];
+
+ // 헤더 스타일
+ const headerRow = worksheet.getRow(1);
+ headerRow.eachCell((cell) => {
+ cell.fill = {
+ type: 'pattern',
+ pattern: 'solid',
+ fgColor: { argb: 'FFFF6B6B' }
+ };
+ cell.font = {
+ bold: true,
+ color: { argb: 'FFFFFFFF' }
+ };
+ cell.border = {
+ top: { style: 'thin' },
+ left: { style: 'thin' },
+ bottom: { style: 'thin' },
+ right: { style: 'thin' }
+ };
+ });
+
+ // 오류 데이터 추가
+ failedRows.forEach((item) => {
+ let solution = '시스템 관리자에게 문의하세요';
+
+ if (item.error.includes('벤더 코드') || item.error.includes('벤더 이메일')) {
+ solution = '등록된 벤더 코드 또는 이메일인지 확인하세요';
+ } else if (item.error.includes('아이템 코드')) {
+ solution = '벤더 타입에 맞는 아이템 코드인지 확인하세요';
+ } else if (item.error.includes('이미 존재')) {
+ solution = '중복된 조합입니다. 제거하거나 건너뛰세요';
+ }
+
+ const row = worksheet.addRow({
+ row: item.row,
+ itemCode: item.itemCode || '누락',
+ vendorCode: item.vendorCode || '누락',
+ vendorEmail: item.vendorEmail || '누락',
+ error: item.error,
+ solution: solution,
+ });
+
+ row.eachCell((cell) => {
+ cell.border = {
+ top: { style: 'thin' },
+ left: { style: 'thin' },
+ bottom: { style: 'thin' },
+ right: { style: 'thin' }
+ };
+ });
+ });
+
+ // 안내사항 추가
+ const instructionSheet = workbook.addWorksheet('오류 해결 가이드');
+ const instructions = [
+ ['📋 오류 유형별 해결 방법', ''],
+ ['', ''],
+ ['1. 벤더 코드/이메일 오류:', ''],
+ [' • 시스템에 등록된 벤더 코드 또는 이메일인지 확인', ''],
+ [' • 벤더 관리 메뉴에서 등록 상태 확인', ''],
+ [' • 벤더 코드가 없으면 벤더 이메일로 대체 가능', ''],
+ ['', ''],
+ ['2. 아이템 코드 오류:', ''],
+ [' • 벤더 타입과 일치하는 아이템인지 확인', ''],
+ [' • 조선 벤더 → item_shipbuilding 테이블', ''],
+ [' • 해양TOP 벤더 → item_offshore_top 테이블', ''],
+ [' • 해양HULL 벤더 → item_offshore_hull 테이블', ''],
+ ['', ''],
+ ['3. 중복 오류:', ''],
+ [' • 이미 등록된 벤더-아이템 조합', ''],
+ [' • 기존 데이터 확인 후 중복 제거', ''],
+ ['', ''],
+ ['📞 추가 문의: 시스템 관리자', ''],
+ ];
+
+ instructions.forEach((rowData, index) => {
+ const row = instructionSheet.addRow(rowData);
+ if (index === 0) {
+ row.getCell(1).font = { bold: true, size: 14, color: { argb: 'FF1F4E79' } };
+ } else if (rowData[0]?.includes(':')) {
+ row.getCell(1).font = { bold: true, color: { argb: 'FF1F4E79' } };
+ }
+ });
+
+ instructionSheet.getColumn(1).width = 50;
+
+ // 파일 생성 및 다운로드
+ const buffer = await workbook.xlsx.writeBuffer();
+ const blob = new Blob([buffer], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+ });
+
+ const fileName = `Import_오류_${new Date().toISOString().split('T')[0]}_${Date.now()}.xlsx`;
+ saveAs(blob, fileName);
+
+ return fileName;
+ } catch (error) {
+ console.error("오류 파일 생성 중 오류:", error);
+ return '';
+ }
+}
\ No newline at end of file diff --git a/lib/tech-vendor-possible-items/table/excel-template.tsx b/lib/tech-vendor-possible-items/table/excel-template.tsx new file mode 100644 index 00000000..70a7eddf --- /dev/null +++ b/lib/tech-vendor-possible-items/table/excel-template.tsx @@ -0,0 +1,137 @@ +import * as ExcelJS from 'exceljs';
+import { saveAs } from "file-saver";
+
+/**
+ * 기술영업 벤더 가능 아이템 Import를 위한 Excel 템플릿 파일 생성 및 다운로드
+ */
+export async function exportTechVendorPossibleItemsTemplate() {
+ // 워크북 생성
+ const workbook = new ExcelJS.Workbook();
+ workbook.creator = 'Tech Vendor Possible Items Management System';
+ workbook.created = new Date();
+
+ // 워크시트 생성
+ const worksheet = workbook.addWorksheet('기술영업 벤더 가능 아이템');
+
+ // 컬럼 헤더 정의 및 스타일 적용
+ worksheet.columns = [
+ { header: '아이템코드', key: 'itemCode', width: 20 },
+ { header: '벤더코드', key: 'vendorCode', width: 15 },
+ { header: '벤더이메일', key: 'vendorEmail', width: 30 },
+ ];
+
+ // 헤더 스타일 적용
+ const headerRow = worksheet.getRow(1);
+ headerRow.eachCell((cell) => {
+ cell.fill = {
+ type: 'pattern',
+ pattern: 'solid',
+ fgColor: { argb: 'FFE6F3FF' }
+ };
+ cell.font = {
+ bold: true,
+ color: { argb: 'FF1F4E79' }
+ };
+ cell.border = {
+ top: { style: 'thin' },
+ left: { style: 'thin' },
+ bottom: { style: 'thin' },
+ right: { style: 'thin' }
+ };
+ cell.alignment = {
+ vertical: 'middle',
+ horizontal: 'center'
+ };
+ });
+
+ // 샘플 데이터 추가
+ const sampleData = [
+ { itemCode: 'ITEM001', vendorCode: 'V001', vendorEmail: '' },
+ { itemCode: 'ITEM001', vendorCode: 'V002', vendorEmail: '' },
+ { itemCode: 'ITEM002', vendorCode: '', vendorEmail: 'vendor@example.com' },
+ { itemCode: 'ITEM002', vendorCode: 'V002', vendorEmail: '' },
+ { itemCode: 'ITEM004', vendorCode: '', vendorEmail: 'vendor2@example.com' },
+ ];
+
+ sampleData.forEach((data) => {
+ const row = worksheet.addRow(data);
+ row.eachCell((cell) => {
+ cell.border = {
+ top: { style: 'thin' },
+ left: { style: 'thin' },
+ bottom: { style: 'thin' },
+ right: { style: 'thin' }
+ };
+ cell.alignment = {
+ vertical: 'middle',
+ horizontal: 'left'
+ };
+ });
+ });
+
+ // 안내사항 워크시트 생성
+ const guideSheet = workbook.addWorksheet('사용 가이드');
+
+ const guideData = [
+ ['기술영업 벤더 가능 아이템 Import 템플릿', ''],
+ ['', ''],
+ ['📋 사용 방법:', ''],
+ ['1. "기술영업 벤더 가능 아이템" 시트에 데이터를 입력하세요', ''],
+ ['2. 벤더 식별: 벤더코드 또는 벤더이메일 중 하나는 반드시 입력', ''],
+ [' • 벤더코드가 있으면 벤더코드를 우선 사용', ''],
+ [' • 벤더코드가 없으면 벤더이메일로 벤더 검색', ''],
+ ['3. 아이템코드는 실제 존재하는 아이템코드를 사용하세요', ''],
+ ['4. 한 아이템코드에 여러 벤더를 매핑할 수 있습니다 (1:N 관계)', ''],
+ ['5. 중복된 벤더-아이템 조합은 무시됩니다', ''],
+ ['6. 파일 저장 후 시스템에서 업로드하세요', ''],
+ ['', ''],
+ ['⚠️ 중요 사항:', ''],
+ ['- 벤더코드 또는 벤더이메일 중 하나는 반드시 필요', ''],
+ ['- 벤더코드가 우선, 없으면 벤더이메일로 검색', ''],
+ ['- 중복된 벤더-아이템 조합은 건너뜁니다', ''],
+ ['- 오류가 있는 항목은 별도 파일로 다운로드됩니다', ''],
+ ['- 빈 셀이 있으면 해당 행은 무시됩니다', ''],
+ ['', ''],
+ ['💡 팁:', ''],
+ ['- 벤더코드만 존재하면 어떤 아이템코드든 입력 가능합니다', ''],
+ ['- 아이템코드는 그대로 시스템에 저장됩니다', ''],
+ ['', ''],
+ ['📞 문의사항이 있으시면 시스템 관리자에게 연락하세요.', ''],
+ ];
+
+ guideData.forEach((rowData, index) => {
+ const row = guideSheet.addRow(rowData);
+ if (index === 0) {
+ // 제목 스타일
+ row.getCell(1).font = { bold: true, size: 16, color: { argb: 'FF1F4E79' } };
+ } else if (rowData[0]?.includes(':')) {
+ // 섹션 제목 스타일
+ row.getCell(1).font = { bold: true, color: { argb: 'FF1F4E79' } };
+ } else if (rowData[0]?.includes('•') || rowData[0]?.includes('-')) {
+ // 리스트 아이템 스타일
+ row.getCell(1).font = { color: { argb: 'FF333333' } };
+ }
+ });
+
+ guideSheet.getColumn(1).width = 70;
+ guideSheet.getColumn(2).width = 20;
+
+ // 파일 생성 및 다운로드
+ try {
+ const buffer = await workbook.xlsx.writeBuffer();
+ const blob = new Blob([buffer], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+ });
+
+ const fileName = `기술영업_벤더_가능_아이템_템플릿_${new Date().toISOString().split('T')[0]}.xlsx`;
+ saveAs(blob, fileName);
+
+ return { success: true };
+ } catch (error) {
+ console.error("Excel 템플릿 생성 중 오류:", error);
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "템플릿 생성 중 오류가 발생했습니다."
+ };
+ }
+}
\ No newline at end of file diff --git a/lib/tech-vendor-possible-items/table/possible-items-data-table.tsx b/lib/tech-vendor-possible-items/table/possible-items-data-table.tsx new file mode 100644 index 00000000..5252684b --- /dev/null +++ b/lib/tech-vendor-possible-items/table/possible-items-data-table.tsx @@ -0,0 +1,90 @@ +"use client";
+
+import * as React from "react";
+
+import { useDataTable } from "@/hooks/use-data-table";
+import { DataTable } from "@/components/data-table/data-table";
+import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar";
+
+import { getColumns } from "./possible-items-table-columns";
+import { PossibleItemsTableToolbarActions } from "./possible-items-table-toolbar-actions";
+
+// 타입만 import
+type TechVendorPossibleItemsData = {
+ id: number;
+ vendorId: number;
+ vendorCode: string | null;
+ vendorName: string;
+ techVendorType: string;
+ itemCode: string;
+ createdAt: Date;
+ updatedAt: Date;
+};
+import type { DataTableAdvancedFilterField } from "@/types/table";
+
+interface PossibleItemsDataTableProps {
+ promises: Promise<[{
+ data: TechVendorPossibleItemsData[];
+ pageCount: number;
+ totalCount: number;
+ }, string[]]>;
+}
+
+export function PossibleItemsDataTable({ promises }: PossibleItemsDataTableProps) {
+ const [{ data, pageCount }, vendorTypes] = React.use(promises);
+
+ const columns = React.useMemo(() => getColumns(), []);
+
+ const filterFields: DataTableAdvancedFilterField<TechVendorPossibleItemsData>[] = [
+ {
+ id: "vendorCode",
+ label: "벤더코드",
+ type: "text",
+ },
+ {
+ id: "vendorName",
+ label: "벤더명",
+ type: "text",
+ },
+ {
+ id: "itemCode",
+ label: "아이템코드",
+ type: "text",
+ },
+ {
+ id: "techVendorType",
+ label: "벤더타입",
+ type: "multi-select",
+ options: Array.isArray(vendorTypes) ? vendorTypes.map((type: string) => ({
+ label: type,
+ value: type,
+ count: 0,
+ })) : [],
+ },
+ ];
+
+ const { table } = useDataTable({
+ data,
+ columns,
+ pageCount,
+ filterFields,
+ enableAdvancedFilter: true,
+ initialState: {
+ sorting: [{ id: "createdAt", desc: true }],
+ pagination: { pageIndex: 0, pageSize: 10 },
+ },
+ getRowId: (originalRow) => String(originalRow.id),
+ shallow: false,
+ clearOnDefault: true,
+ });
+
+ return (
+ <>
+ <DataTable table={table}>
+ <DataTableAdvancedToolbar table={table} filterFields={filterFields}>
+ <PossibleItemsTableToolbarActions table={table} />
+ </DataTableAdvancedToolbar>
+ </DataTable>
+ </>
+ );
+}
\ No newline at end of file diff --git a/lib/tech-vendor-possible-items/table/possible-items-table-columns.tsx b/lib/tech-vendor-possible-items/table/possible-items-table-columns.tsx new file mode 100644 index 00000000..520c089e --- /dev/null +++ b/lib/tech-vendor-possible-items/table/possible-items-table-columns.tsx @@ -0,0 +1,140 @@ +"use client";
+
+import { ColumnDef } from "@tanstack/react-table";
+import { Checkbox } from "@/components/ui/checkbox";
+import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header";
+import Link from "next/link";
+// 타입만 import
+type TechVendorPossibleItemsData = {
+ id: number;
+ vendorId: number;
+ vendorCode: string | null;
+ vendorName: string;
+ techVendorType: string;
+ itemCode: string;
+ createdAt: Date;
+ updatedAt: Date;
+};
+import { format } from "date-fns";
+import { ko } from "date-fns/locale";
+import { Badge } from "@/components/ui/badge";
+
+export function getColumns(): ColumnDef<TechVendorPossibleItemsData>[] {
+ return [
+ {
+ id: "select",
+ header: ({ table }) => (
+ <Checkbox
+ checked={
+ table.getIsAllPageRowsSelected() ||
+ (table.getIsSomePageRowsSelected() && "indeterminate")
+ }
+ onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
+ aria-label="모두 선택"
+ className="translate-y-[2px]"
+ />
+ ),
+ cell: ({ row }) => (
+ <Checkbox
+ checked={row.getIsSelected()}
+ onCheckedChange={(value) => row.toggleSelected(!!value)}
+ aria-label="행 선택"
+ className="translate-y-[2px]"
+ />
+ ),
+ enableSorting: false,
+ enableHiding: false,
+ },
+ {
+ accessorKey: "itemCode",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="아이템코드" />
+ ),
+ cell: ({ row }) => {
+ const itemCode = row.getValue("itemCode") as string;
+ return <div className="font-medium">{itemCode}</div>;
+ },
+ },
+ {
+ accessorKey: "vendorCode",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="벤더코드" />
+ ),
+ cell: ({ row }) => {
+ const vendorCode = row.getValue("vendorCode") as string;
+ return <div className="font-medium">{vendorCode || "-"}</div>;
+ },
+ },
+ {
+ accessorKey: "vendorName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="벤더명" />
+ ),
+ cell: ({ row }) => {
+ const vendorName = row.getValue("vendorName") as string;
+ const vendorId = row.original.vendorId;
+ return (
+ <Link
+ href={`/ko/evcp/tech-vendors/${vendorId}/info`}
+ className="max-w-[200px] truncate hover:underline"
+ >
+ {vendorName}
+ </Link>
+ );
+ },
+ },
+ {
+ accessorKey: "techVendorType",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="벤더타입" />
+ ),
+ cell: ({ row }) => {
+ const techVendorType = row.getValue("techVendorType") as string;
+
+ // JSON 배열인지 확인하고 파싱
+ let types: string[] = [];
+ try {
+ const parsed = JSON.parse(techVendorType || "[]");
+ types = Array.isArray(parsed) ? parsed : [techVendorType];
+ } catch {
+ types = [techVendorType];
+ }
+
+ return (
+ <div className="flex flex-wrap gap-1">
+ {types.map((type, index) => (
+ <Badge key={index} variant="secondary" className="text-xs">
+ {type}
+ </Badge>
+ ))}
+ </div>
+ );
+ },
+ filterFn: (row, id, value) => {
+ const techVendorType = row.getValue(id) as string;
+ try {
+ const parsed = JSON.parse(techVendorType || "[]");
+ const types = Array.isArray(parsed) ? parsed : [techVendorType];
+ return types.some(type => type.includes(value));
+ } catch {
+ return techVendorType?.includes(value) || false;
+ }
+ },
+ },
+
+ {
+ accessorKey: "createdAt",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="생성일시" />
+ ),
+ cell: ({ row }) => {
+ const createdAt = row.getValue("createdAt") as Date;
+ return (
+ <div className="text-sm text-muted-foreground">
+ {format(createdAt, "yyyy-MM-dd HH:mm", { locale: ko })}
+ </div>
+ );
+ },
+ },
+ ];
+}
\ No newline at end of file diff --git a/lib/tech-vendor-possible-items/table/possible-items-table-toolbar-actions.tsx b/lib/tech-vendor-possible-items/table/possible-items-table-toolbar-actions.tsx new file mode 100644 index 00000000..3628f87e --- /dev/null +++ b/lib/tech-vendor-possible-items/table/possible-items-table-toolbar-actions.tsx @@ -0,0 +1,201 @@ +"use client";
+
+import * as React from "react";
+import { type Table } from "@tanstack/react-table";
+import { Download, Upload, FileSpreadsheet, Trash2 } from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { useToast } from "@/hooks/use-toast";
+import { deleteTechVendorPossibleItems } from "@/lib/tech-vendor-possible-items/service";
+// Excel 함수들을 동적 import로만 사용하기 위해 타입만 import
+type TechVendorPossibleItemsData = {
+ id: number;
+ vendorId: number;
+ vendorCode: string | null;
+ vendorName: string;
+ techVendorType: string;
+ itemCode: string;
+ createdAt: Date;
+ updatedAt: Date;
+};
+
+interface PossibleItemsTableToolbarActionsProps {
+ table: Table<TechVendorPossibleItemsData>;
+}
+
+export function PossibleItemsTableToolbarActions({
+ table,
+}: PossibleItemsTableToolbarActionsProps) {
+ const { toast } = useToast();
+ const [isPending, startTransition] = React.useTransition();
+
+ const selectedRows = table.getFilteredSelectedRowModel().rows;
+ const hasSelection = selectedRows.length > 0;
+
+ const handleDelete = () => {
+ if (!hasSelection) return;
+
+ startTransition(async () => {
+ const selectedIds = selectedRows.map((row) => row.original.id);
+
+ try {
+ const result = await deleteTechVendorPossibleItems(selectedIds);
+
+ if (result.success) {
+ toast({
+ title: "성공",
+ description: `${selectedIds.length}개의 아이템이 삭제되었습니다.`,
+ });
+ table.toggleAllRowsSelected(false);
+ // 페이지 새로고침이나 데이터 다시 로드 필요
+ window.location.reload();
+ } else {
+ toast({
+ title: "오류",
+ description: result.error || "삭제 중 오류가 발생했습니다.",
+ variant: "destructive",
+ });
+ }
+ } catch (error) {
+ console.error("Delete error:", error);
+ toast({
+ title: "오류",
+ description: "삭제 중 오류가 발생했습니다.",
+ variant: "destructive",
+ });
+ }
+ });
+ };
+
+ const handleExport = async () => {
+ try {
+ const { exportTechVendorPossibleItemsToExcel } = await import("./excel-export");
+ const result = await exportTechVendorPossibleItemsToExcel(table.getFilteredRowModel().rows.map(row => row.original));
+
+ if (result.success) {
+ toast({
+ title: "성공",
+ description: "Excel 파일이 다운로드되었습니다.",
+ });
+ } else {
+ toast({
+ title: "오류",
+ description: result.error || "내보내기 중 오류가 발생했습니다.",
+ variant: "destructive",
+ });
+ }
+ } catch (error) {
+ console.error("Export error:", error);
+ toast({
+ title: "오류",
+ description: "내보내기 중 오류가 발생했습니다.",
+ variant: "destructive",
+ });
+ }
+ };
+
+ const handleImport = async (event: React.ChangeEvent<HTMLInputElement>) => {
+ const file = event.target.files?.[0];
+ if (!file) return;
+
+ try {
+ const { importTechVendorPossibleItemsFromExcel } = await import("./excel-import");
+ const result = await importTechVendorPossibleItemsFromExcel(file);
+
+ if (result.success) {
+ toast({
+ title: "성공",
+ description: `${result.successCount}개의 아이템이 가져와졌습니다.`,
+ });
+ // 페이지 새로고침이나 데이터 다시 로드 필요
+ window.location.reload();
+ } else {
+ toast({
+ title: "가져오기 완료",
+ description: `${result.successCount}개 성공, ${result.failedRows.length}개 실패`,
+ variant: result.successCount > 0 ? "default" : "destructive",
+ });
+ }
+ } catch (error) {
+ console.error("Import error:", error);
+ toast({
+ title: "오류",
+ description: "가져오기 중 오류가 발생했습니다.",
+ variant: "destructive",
+ });
+ }
+
+ // Reset input
+ event.target.value = "";
+ };
+
+ const handleDownloadTemplate = async () => {
+ try {
+ const { exportTechVendorPossibleItemsTemplate } = await import("./excel-template");
+ const result = await exportTechVendorPossibleItemsTemplate();
+ if (result.success) {
+ toast({
+ title: "성공",
+ description: "템플릿 파일이 다운로드되었습니다.",
+ });
+ } else {
+ toast({
+ title: "오류",
+ description: result.error || "템플릿 다운로드 중 오류가 발생했습니다.",
+ variant: "destructive",
+ });
+ }
+ } catch (error) {
+ console.error("Template download error:", error);
+ toast({
+ title: "오류",
+ description: "템플릿 다운로드 중 오류가 발생했습니다.",
+ variant: "destructive",
+ });
+ }
+ };
+
+ return (
+ <div className="flex items-center gap-2">
+ {hasSelection && (
+ <Button
+ variant="destructive"
+ size="sm"
+ onClick={handleDelete}
+ disabled={isPending}
+ >
+ <Trash2 className="mr-2 h-4 w-4" />
+ 삭제 ({selectedRows.length})
+ </Button>
+ )}
+
+ <Button variant="outline" size="sm" onClick={handleExport}>
+ <Download className="mr-2 h-4 w-4" />
+ Export
+ </Button>
+
+ <Input
+ id="import-file"
+ type="file"
+ accept=".xlsx,.xls"
+ onChange={handleImport}
+ className="hidden"
+ />
+
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => document.getElementById("import-file")?.click()}
+ >
+ <Upload className="mr-2 h-4 w-4" />
+ Import
+ </Button>
+
+ <Button variant="outline" size="sm" onClick={handleDownloadTemplate}>
+ <FileSpreadsheet className="mr-2 h-4 w-4" />
+ Download Template
+ </Button>
+ </div>
+ );
+}
\ No newline at end of file |
