Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feat/dashboard

This commit is contained in:
dohyeons
2025-11-04 09:44:09 +09:00
16 changed files with 2428 additions and 727 deletions

View File

@@ -410,6 +410,128 @@ export class DynamicFormApi {
};
}
}
/**
* 테이블 데이터 조회 (페이징 + 검색)
* @param tableName 테이블명
* @param params 검색 파라미터
* @returns 테이블 데이터
*/
static async getTableData(
tableName: string,
params?: {
page?: number;
pageSize?: number;
search?: string;
sortBy?: string;
sortOrder?: "asc" | "desc";
filters?: Record<string, any>;
},
): Promise<ApiResponse<any[]>> {
try {
console.log("📊 테이블 데이터 조회 요청:", { tableName, params });
const response = await apiClient.post(`/table-management/tables/${tableName}/data`, params || {});
console.log("✅ 테이블 데이터 조회 성공 (원본):", response.data);
console.log("🔍 response.data 상세:", {
type: typeof response.data,
isArray: Array.isArray(response.data),
keys: response.data ? Object.keys(response.data) : [],
hasData: response.data?.data !== undefined,
dataType: response.data?.data ? typeof response.data.data : "N/A",
dataIsArray: response.data?.data ? Array.isArray(response.data.data) : false,
dataLength: response.data?.data ? (Array.isArray(response.data.data) ? response.data.data.length : "not array") : "no data",
// 중첩 구조 확인
dataDataExists: response.data?.data?.data !== undefined,
dataDataIsArray: response.data?.data?.data ? Array.isArray(response.data.data.data) : false,
dataDataLength: response.data?.data?.data ? (Array.isArray(response.data.data.data) ? response.data.data.data.length : "not array") : "no nested data",
});
// API 응답 구조: { data: [...], total, page, size, totalPages }
// 또는 중첩: { success: true, data: { data: [...], total, ... } }
// data 배열만 추출
let tableData: any[] = [];
if (Array.isArray(response.data)) {
// 케이스 1: 응답이 배열이면 그대로 사용
console.log("✅ 케이스 1: 응답이 배열");
tableData = response.data;
} else if (response.data && Array.isArray(response.data.data)) {
// 케이스 2: 응답이 { data: [...] } 구조면 data 배열 추출
console.log("✅ 케이스 2: 응답이 { data: [...] } 구조");
tableData = response.data.data;
} else if (response.data?.data?.data && Array.isArray(response.data.data.data)) {
// 케이스 2-1: 중첩 구조 { success: true, data: { data: [...] } }
console.log("✅ 케이스 2-1: 중첩 구조 { data: { data: [...] } }");
tableData = response.data.data.data;
} else if (response.data && typeof response.data === "object") {
// 케이스 3: 응답이 객체면 배열로 감싸기 (최후의 수단)
console.log("⚠️ 케이스 3: 응답이 객체 (배열로 감싸기)");
tableData = [response.data];
}
console.log("✅ 테이블 데이터 추출 완료:", {
originalType: typeof response.data,
isArray: Array.isArray(response.data),
hasDataProperty: response.data?.data !== undefined,
extractedCount: tableData.length,
firstRow: tableData[0],
allRows: tableData,
});
return {
success: true,
data: tableData,
message: "테이블 데이터 조회가 완료되었습니다.",
};
} catch (error: any) {
console.error("❌ 테이블 데이터 조회 실패:", error);
const errorMessage = error.response?.data?.message || error.message || "테이블 데이터 조회 중 오류가 발생했습니다.";
return {
success: false,
message: errorMessage,
errorCode: error.response?.data?.errorCode,
};
}
}
/**
* 엑셀 업로드 (대량 데이터 삽입/업데이트)
* @param payload 업로드 데이터
* @returns 업로드 결과
*/
static async uploadExcelData(payload: {
tableName: string;
data: any[];
uploadMode: "insert" | "update" | "upsert";
keyColumn?: string;
}): Promise<ApiResponse<any>> {
try {
console.log("📤 엑셀 업로드 요청:", payload);
const response = await apiClient.post(`/dynamic-form/excel-upload`, payload);
console.log("✅ 엑셀 업로드 성공:", response.data);
return {
success: true,
data: response.data,
message: "엑셀 파일이 성공적으로 업로드되었습니다.",
};
} catch (error: any) {
console.error("❌ 엑셀 업로드 실패:", error);
const errorMessage = error.response?.data?.message || error.message || "엑셀 업로드 중 오류가 발생했습니다.";
return {
success: false,
message: errorMessage,
errorCode: error.response?.data?.errorCode,
};
}
}
}
// 편의를 위한 기본 export

View File

@@ -247,8 +247,8 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
// 추가 안전장치: 모든 로딩 토스트 제거
toast.dismiss();
// UI 전환 액션(edit, modal, navigate)을 제외하고만 로딩 토스트 표시
const silentActions = ["edit", "modal", "navigate"];
// UI 전환 액션 및 모달 액션은 로딩 토스트 표시하지 않음
const silentActions = ["edit", "modal", "navigate", "excel_upload", "barcode_scan"];
if (!silentActions.includes(actionConfig.type)) {
currentLoadingToastRef.current = toast.loading(
actionConfig.type === "save"
@@ -274,9 +274,9 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
// 실패한 경우 오류 처리
if (!success) {
// UI 전환 액션(edit, modal, navigate)은 에러도 조용히 처리
const silentActions = ["edit", "modal", "navigate"];
if (silentActions.includes(actionConfig.type)) {
// UI 전환 액션 및 모달 액션은 에러도 조용히 처리 (모달 내부에서 자체 에러 표시)
const silentErrorActions = ["edit", "modal", "navigate", "excel_upload", "barcode_scan"];
if (silentErrorActions.includes(actionConfig.type)) {
return;
}
// 기본 에러 메시지 결정
@@ -302,8 +302,10 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
}
// 성공한 경우에만 성공 토스트 표시
// edit, modal, navigate 액션은 조용히 처리 (UI 전환만 하므로 토스트 불필요)
if (actionConfig.type !== "edit" && actionConfig.type !== "modal" && actionConfig.type !== "navigate") {
// edit, modal, navigate, excel_upload, barcode_scan 액션은 조용히 처리
// (UI 전환만 하거나 모달 내부에서 자체적으로 메시지 표시)
const silentSuccessActions = ["edit", "modal", "navigate", "excel_upload", "barcode_scan"];
if (!silentSuccessActions.includes(actionConfig.type)) {
// 기본 성공 메시지 결정
const defaultSuccessMessage =
actionConfig.type === "save"

View File

@@ -284,7 +284,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// 컬럼 라벨 가져오기
// ========================================
const fetchColumnLabels = async () => {
const fetchColumnLabels = useCallback(async () => {
if (!tableConfig.selectedTable) return;
try {
@@ -339,13 +339,13 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
} catch (error) {
console.error("컬럼 라벨 가져오기 실패:", error);
}
};
}, [tableConfig.selectedTable]);
// ========================================
// 테이블 라벨 가져오기
// ========================================
const fetchTableLabel = async () => {
const fetchTableLabel = useCallback(async () => {
if (!tableConfig.selectedTable) return;
try {
@@ -374,7 +374,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
} catch (error) {
console.error("테이블 라벨 가져오기 실패:", error);
}
};
}, [tableConfig.selectedTable]);
// ========================================
// 데이터 가져오기
@@ -531,7 +531,10 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
onSelectedRowsChange(Array.from(newSelectedRows), selectedRowsData);
}
if (onFormDataChange) {
onFormDataChange({ selectedRows: Array.from(newSelectedRows), selectedRowsData });
onFormDataChange({
selectedRows: Array.from(newSelectedRows),
selectedRowsData,
});
}
const allRowsSelected = data.every((row, index) => newSelectedRows.has(getRowKey(row, index)));
@@ -549,7 +552,10 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
onSelectedRowsChange(Array.from(newSelectedRows), data);
}
if (onFormDataChange) {
onFormDataChange({ selectedRows: Array.from(newSelectedRows), selectedRowsData: data });
onFormDataChange({
selectedRows: Array.from(newSelectedRows),
selectedRowsData: data,
});
}
} else {
setSelectedRows(new Set());
@@ -930,7 +936,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
useEffect(() => {
fetchColumnLabels();
fetchTableLabel();
}, [tableConfig.selectedTable]);
}, [tableConfig.selectedTable, fetchColumnLabels, fetchTableLabel]);
useEffect(() => {
if (!isDesignMode && tableConfig.selectedTable) {
@@ -945,6 +951,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
searchTerm,
refreshKey,
isDesignMode,
fetchTableDataDebounced,
]);
useEffect(() => {

View File

@@ -17,7 +17,10 @@ export type ButtonActionType =
| "navigate" // 페이지 이동
| "modal" // 모달 열기
| "control" // 제어 흐름
| "view_table_history"; // 테이블 이력 보기
| "view_table_history" // 테이블 이력 보기
| "excel_download" // 엑셀 다운로드
| "excel_upload" // 엑셀 업로드
| "barcode_scan"; // 바코드 스캔
/**
* 버튼 액션 설정
@@ -56,6 +59,20 @@ export interface ButtonActionConfig {
historyRecordIdSource?: "selected_row" | "form_field" | "context"; // 레코드 ID 가져올 소스
historyRecordLabelField?: string; // 레코드 라벨로 표시할 필드 (선택사항)
historyDisplayColumn?: string; // 전체 이력에서 레코드 구분용 컬럼 (예: device_code, name)
// 엑셀 다운로드 관련
excelFileName?: string; // 다운로드할 파일명 (기본: 테이블명_날짜.xlsx)
excelSheetName?: string; // 시트명 (기본: "Sheet1")
excelIncludeHeaders?: boolean; // 헤더 포함 여부 (기본: true)
// 엑셀 업로드 관련
excelUploadMode?: "insert" | "update" | "upsert"; // 업로드 모드
excelKeyColumn?: string; // 업데이트/Upsert 시 키 컬럼
// 바코드 스캔 관련
barcodeTargetField?: string; // 스캔 결과를 입력할 필드명
barcodeFormat?: "all" | "1d" | "2d"; // 바코드 포맷 (기본: "all")
barcodeAutoSubmit?: boolean; // 스캔 후 자동 제출 여부
}
/**
@@ -121,6 +138,15 @@ export class ButtonActionExecutor {
case "view_table_history":
return this.handleViewTableHistory(config, context);
case "excel_download":
return await this.handleExcelDownload(config, context);
case "excel_upload":
return await this.handleExcelUpload(config, context);
case "barcode_scan":
return await this.handleBarcodeScan(config, context);
default:
console.warn(`지원되지 않는 액션 타입: ${config.type}`);
return false;
@@ -203,15 +229,29 @@ export class ButtonActionExecutor {
// INSERT 처리
// 🆕 자동으로 작성자 정보 추가
const writerValue = context.userId || context.userName || "unknown";
if (!context.userId) {
throw new Error("사용자 정보를 불러올 수 없습니다. 다시 로그인해주세요.");
}
const writerValue = context.userId;
const companyCodeValue = context.companyCode || "";
console.log("👤 [buttonActions] 사용자 정보:", {
userId: context.userId,
userName: context.userName,
companyCode: context.companyCode, // ✅ 회사 코드
formDataWriter: formData.writer, // ✅ 폼에서 입력한 writer 값
formDataCompanyCode: formData.company_code, // ✅ 폼에서 입력한 company_code 값
defaultWriterValue: writerValue,
companyCodeValue, // ✅ 최종 회사 코드 값
});
const dataWithUserInfo = {
...formData,
writer: writerValue,
created_by: writerValue,
updated_by: writerValue,
company_code: companyCodeValue,
writer: formData.writer || writerValue, // ✅ 입력값 우선, 없으면 userId
created_by: writerValue, // created_by는 항상 로그인한 사람
updated_by: writerValue, // updated_by는 항상 로그인한 사람
company_code: formData.company_code || companyCodeValue, // ✅ 입력값 우선, 없으면 user.companyCode
};
saveResult = await DynamicFormApi.saveFormData({
@@ -1632,6 +1672,226 @@ export class ButtonActionExecutor {
}
}
/**
* 엑셀 다운로드 액션 처리
*/
private static async handleExcelDownload(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
try {
console.log("📥 엑셀 다운로드 시작:", { config, context });
// 동적 import로 엑셀 유틸리티 로드
const { exportToExcel } = await import("@/lib/utils/excelExport");
let dataToExport: any[] = [];
// 1순위: 선택된 행 데이터
if (context.selectedRowsData && context.selectedRowsData.length > 0) {
dataToExport = context.selectedRowsData;
console.log("✅ 선택된 행 데이터 사용:", dataToExport.length);
}
// 2순위: 테이블 전체 데이터 (API 호출)
else if (context.tableName) {
console.log("🔄 테이블 전체 데이터 조회 중...", context.tableName);
try {
const { dynamicFormApi } = await import("@/lib/api/dynamicForm");
const response = await dynamicFormApi.getTableData(context.tableName, {
page: 1,
pageSize: 10000, // 최대 10,000개 행
sortBy: "id", // 기본 정렬: id 컬럼
sortOrder: "asc", // 오름차순
});
console.log("📦 API 응답 구조:", {
response,
responseSuccess: response.success,
responseData: response.data,
responseDataType: typeof response.data,
responseDataIsArray: Array.isArray(response.data),
responseDataLength: Array.isArray(response.data) ? response.data.length : "N/A",
});
if (response.success && response.data) {
dataToExport = response.data;
console.log("✅ 테이블 전체 데이터 조회 완료:", {
count: dataToExport.length,
firstRow: dataToExport[0],
});
} else {
console.error("❌ API 응답에 데이터가 없습니다:", response);
}
} catch (error) {
console.error("❌ 테이블 데이터 조회 실패:", error);
}
}
// 4순위: 폼 데이터
else if (context.formData && Object.keys(context.formData).length > 0) {
dataToExport = [context.formData];
console.log("✅ 폼 데이터 사용:", dataToExport);
}
console.log("📊 최종 다운로드 데이터:", {
selectedRowsData: context.selectedRowsData,
selectedRowsLength: context.selectedRowsData?.length,
formData: context.formData,
tableName: context.tableName,
dataToExport,
dataToExportType: typeof dataToExport,
dataToExportIsArray: Array.isArray(dataToExport),
dataToExportLength: Array.isArray(dataToExport) ? dataToExport.length : "N/A",
});
// 배열이 아니면 배열로 변환
if (!Array.isArray(dataToExport)) {
console.warn("⚠️ dataToExport가 배열이 아닙니다. 변환 시도:", dataToExport);
// 객체인 경우 배열로 감싸기
if (typeof dataToExport === "object" && dataToExport !== null) {
dataToExport = [dataToExport];
} else {
toast.error("다운로드할 데이터 형식이 올바르지 않습니다.");
return false;
}
}
if (dataToExport.length === 0) {
toast.error("다운로드할 데이터가 없습니다.");
return false;
}
// 파일명 생성
const fileName = config.excelFileName || `${context.tableName || "데이터"}_${new Date().toISOString().split("T")[0]}.xlsx`;
const sheetName = config.excelSheetName || "Sheet1";
const includeHeaders = config.excelIncludeHeaders !== false;
console.log("📥 엑셀 다운로드 실행:", {
fileName,
sheetName,
includeHeaders,
dataCount: dataToExport.length,
firstRow: dataToExport[0],
});
// 엑셀 다운로드 실행
await exportToExcel(dataToExport, fileName, sheetName, includeHeaders);
toast.success(config.successMessage || "엑셀 파일이 다운로드되었습니다.");
return true;
} catch (error) {
console.error("❌ 엑셀 다운로드 실패:", error);
toast.error(config.errorMessage || "엑셀 다운로드 중 오류가 발생했습니다.");
return false;
}
}
/**
* 엑셀 업로드 액션 처리
*/
private static async handleExcelUpload(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
try {
console.log("📤 엑셀 업로드 모달 열기:", { config, context });
// 동적 import로 모달 컴포넌트 로드
const { ExcelUploadModal } = await import("@/components/common/ExcelUploadModal");
const { createRoot } = await import("react-dom/client");
// 모달 컨테이너 생성
const modalContainer = document.createElement("div");
document.body.appendChild(modalContainer);
const root = createRoot(modalContainer);
const closeModal = () => {
root.unmount();
document.body.removeChild(modalContainer);
};
root.render(
React.createElement(ExcelUploadModal, {
open: true,
onOpenChange: (open: boolean) => {
if (!open) closeModal();
},
tableName: context.tableName || "",
uploadMode: config.excelUploadMode || "insert",
keyColumn: config.excelKeyColumn,
onSuccess: () => {
// 성공 메시지는 ExcelUploadModal 내부에서 이미 표시됨
context.onRefresh?.();
closeModal();
},
}),
);
return true;
} catch (error) {
console.error("❌ 엑셀 업로드 모달 열기 실패:", error);
toast.error(config.errorMessage || "엑셀 업로드 중 오류가 발생했습니다.");
return false;
}
}
/**
* 바코드 스캔 액션 처리
*/
private static async handleBarcodeScan(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
try {
console.log("📷 바코드 스캔 모달 열기:", { config, context });
// 동적 import로 모달 컴포넌트 로드
const { BarcodeScanModal } = await import("@/components/common/BarcodeScanModal");
const { createRoot } = await import("react-dom/client");
// 모달 컨테이너 생성
const modalContainer = document.createElement("div");
document.body.appendChild(modalContainer);
const root = createRoot(modalContainer);
const closeModal = () => {
root.unmount();
document.body.removeChild(modalContainer);
};
root.render(
React.createElement(BarcodeScanModal, {
open: true,
onOpenChange: (open: boolean) => {
if (!open) closeModal();
},
targetField: config.barcodeTargetField,
barcodeFormat: config.barcodeFormat || "all",
autoSubmit: config.barcodeAutoSubmit || false,
onScanSuccess: (barcode: string) => {
console.log("✅ 바코드 스캔 성공:", barcode);
// 대상 필드에 값 입력
if (config.barcodeTargetField && context.onFormDataChange) {
context.onFormDataChange({
...context.formData,
[config.barcodeTargetField]: barcode,
});
}
toast.success(`바코드 스캔 완료: ${barcode}`);
// 자동 제출 옵션이 켜져있으면 저장
if (config.barcodeAutoSubmit) {
this.handleSave(config, context);
}
closeModal();
},
}),
);
return true;
} catch (error) {
console.error("❌ 바코드 스캔 모달 열기 실패:", error);
toast.error("바코드 스캔 중 오류가 발생했습니다.");
return false;
}
}
/**
* 폼 데이터 유효성 검사
*/
@@ -1703,4 +1963,22 @@ export const DEFAULT_BUTTON_ACTIONS: Record<ButtonActionType, Partial<ButtonActi
historyRecordIdField: "id",
historyRecordIdSource: "selected_row",
},
excel_download: {
type: "excel_download",
excelIncludeHeaders: true,
successMessage: "엑셀 파일이 다운로드되었습니다.",
errorMessage: "엑셀 다운로드 중 오류가 발생했습니다.",
},
excel_upload: {
type: "excel_upload",
excelUploadMode: "insert",
confirmMessage: "엑셀 파일을 업로드하시겠습니까?",
successMessage: "엑셀 파일이 업로드되었습니다.",
errorMessage: "엑셀 업로드 중 오류가 발생했습니다.",
},
barcode_scan: {
type: "barcode_scan",
barcodeFormat: "all",
barcodeAutoSubmit: false,
},
};

View File

@@ -0,0 +1,172 @@
/**
* 엑셀 내보내기 유틸리티
* xlsx 라이브러리를 사용하여 데이터를 엑셀 파일로 변환
*/
import * as XLSX from "xlsx";
/**
* 데이터를 엑셀 파일로 내보내기
* @param data 내보낼 데이터 배열
* @param fileName 파일명 (기본: "export.xlsx")
* @param sheetName 시트명 (기본: "Sheet1")
* @param includeHeaders 헤더 포함 여부 (기본: true)
*/
export async function exportToExcel(
data: Record<string, any>[],
fileName: string = "export.xlsx",
sheetName: string = "Sheet1",
includeHeaders: boolean = true
): Promise<void> {
try {
console.log("📥 엑셀 내보내기 시작:", {
dataCount: data.length,
fileName,
sheetName,
includeHeaders,
});
if (data.length === 0) {
throw new Error("내보낼 데이터가 없습니다.");
}
// 워크북 생성
const workbook = XLSX.utils.book_new();
// 데이터를 워크시트로 변환
const worksheet = XLSX.utils.json_to_sheet(data, {
header: includeHeaders ? undefined : [],
skipHeader: !includeHeaders,
});
// 컬럼 너비 자동 조정
const columnWidths = autoSizeColumns(data);
worksheet["!cols"] = columnWidths;
// 워크시트를 워크북에 추가
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
// 파일 다운로드
XLSX.writeFile(workbook, fileName);
console.log("✅ 엑셀 내보내기 완료:", fileName);
} catch (error) {
console.error("❌ 엑셀 내보내기 실패:", error);
throw error;
}
}
/**
* 컬럼 너비 자동 조정
*/
function autoSizeColumns(data: Record<string, any>[]): Array<{ wch: number }> {
if (data.length === 0) return [];
const keys = Object.keys(data[0]);
const columnWidths: Array<{ wch: number }> = [];
keys.forEach((key) => {
// 헤더 길이
let maxWidth = key.length;
// 데이터 길이 확인
data.forEach((row) => {
const value = row[key];
const valueLength = value ? String(value).length : 0;
maxWidth = Math.max(maxWidth, valueLength);
});
// 최소 10, 최대 50으로 제한
columnWidths.push({ wch: Math.min(Math.max(maxWidth, 10), 50) });
});
return columnWidths;
}
/**
* 엑셀 파일을 읽어서 JSON 데이터로 변환
* @param file 읽을 파일
* @param sheetName 읽을 시트명 (기본: 첫 번째 시트)
* @returns JSON 데이터 배열
*/
export async function importFromExcel(
file: File,
sheetName?: string
): Promise<Record<string, any>[]> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = e.target?.result;
if (!data) {
reject(new Error("파일을 읽을 수 없습니다."));
return;
}
// 워크북 읽기
const workbook = XLSX.read(data, { type: "binary" });
// 시트 선택 (지정된 시트 또는 첫 번째 시트)
const targetSheetName = sheetName || workbook.SheetNames[0];
const worksheet = workbook.Sheets[targetSheetName];
if (!worksheet) {
reject(new Error(`시트 "${targetSheetName}"를 찾을 수 없습니다.`));
return;
}
// JSON으로 변환
const jsonData = XLSX.utils.sheet_to_json(worksheet);
console.log("✅ 엑셀 가져오기 완료:", {
sheetName: targetSheetName,
rowCount: jsonData.length,
});
resolve(jsonData as Record<string, any>[]);
} catch (error) {
console.error("❌ 엑셀 가져오기 실패:", error);
reject(error);
}
};
reader.onerror = () => {
reject(new Error("파일 읽기 중 오류가 발생했습니다."));
};
reader.readAsBinaryString(file);
});
}
/**
* 엑셀 파일의 시트 목록 가져오기
*/
export async function getExcelSheetNames(file: File): Promise<string[]> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = e.target?.result;
if (!data) {
reject(new Error("파일을 읽을 수 없습니다."));
return;
}
const workbook = XLSX.read(data, { type: "binary" });
resolve(workbook.SheetNames);
} catch (error) {
console.error("❌ 시트 목록 가져오기 실패:", error);
reject(error);
}
};
reader.onerror = () => {
reject(new Error("파일 읽기 중 오류가 발생했습니다."));
};
reader.readAsBinaryString(file);
});
}