엑셀 파일 업로드 하기

가은·2024년 10월 17일
0
post-thumbnail

회사 관리자 페이지에 엑셀 파일을 업로드하고 화면에 반영하는 걸 구현해야되었는데 처음 해보는 것이였어서 기록하고자 남김!

드래그 앤 드랍과 클릭 이벤트로 파일 업로드가 가능하고, 엑셀 파일이 삭제 되거나 정보가 입력된 행이 다 삭제되면 반대의 것도 자동으로 삭제되게 구현했다.

XLSX : 엑셀 파일 업로드 시 파일 내부 검사를 위해 설치한 라이브러리

코드 설명

const [inputs, setInputs] = useState<InputFields[]>([
  { id: Date.now(), group: '', userId: '', name: '', phoneNum: '' },
]);

엑셀 파일에서 가져온 데이터 입력 및 추가, 수정

const [validationErrors, setValidationErrors] = useState<string[]>([]);
const [validationSuccess, setValidationSuccess] = useState<string | null>(null);

유효성 검사 오류, 성공 메시지 저장 상태

const [isActive, setActive] = useState(false);
const [uploadedInfo, setUploadedInfo] = useState<{
  name: string;
  size: string;
  type: string;
} | null>(null);

isActive: 파일이 업로드 영역에 있는지 여부를 관리하는 상태

uploadedInfo: 업로드한 파일의 정보를 저장

const fileInputRef = useRef<HTMLInputElement | null>(null);

fileInputRef: 파일 입력 요소에 대한 참조 관리

파일 정보 설정

const setFileInfo = (file: File) => {
  if (file) {
    const { name, size: byteSize, type } = file;
    const size = (byteSize / (1024 * 1024)).toFixed(2) + 'mb';
    setUploadedInfo({ name, size, type });
    readExcelFile(file);
  }
};

setFileInfo: 업로드된 파일의 정보를 설정하고, readExcelFile 함수를 호출하여 파일을 읽음 (파일의 크기를 메가바이트 단위로 변환)

→ 파일 정보가 보이는 것은 임의로 해둔 것이라 기획에 맞게 변경

파일 데이터 유효성 검사

validateFileData: 엑셀 파일 데이터를 받아서 유효성 검사를 수행

const validateFileData = (data: Array<Record<string, string>>) => {
  if (data.length === 0) {
    setValidationErrors(['빈 파일 입니다.']);
    setValidationSuccess(null);
    return;
  }
  1. 파일이 비어 있는지 확인
  const requiredColumns = ['이름', '아이디', '휴대전화'];
  const firstRow = data[0];
  const missingColumns = requiredColumns.filter(
    col => !Object.prototype.hasOwnProperty.call(firstRow, col)
  );
  1. 필수로 포함되어야 할 컬럼(이름, 아이디, 휴대전화)이 있는지 확인하고, 누락된 컬럼이 있으면 에러 메시지를 설정
  const invalidRows: string[] = [];
  const userIdCounts: Record<string, number> = {};
  data.forEach((row, index) => {
	   // 검사할 내용 작성
     if (!row['이름'] || typeof row['이름'] !== 'string') {
        invalidRows.push(`Row ${index + 1}: 유효하지 않은 이름`);
      }
      if (!row['아이디'] || typeof row['아이디'] !== 'string') {
        invalidRows.push(`Row ${index + 1}: 유효하지 않은 아이디`);
      }

      const userId = row['아이디'];
      if (userId) {
        userIdCounts[userId] = (userIdCounts[userId] || 0) + 1;
      }

      const phoneNumberPattern = /^[0-9]{10,15}$/;
      if (!row['휴대전화'] || !phoneNumberPattern.test(row['휴대전화'])) {
        invalidRows.push(`Row ${index + 1}: 유효하지 않은 휴대전화 번호`);
      }
    });

    Object.keys(userIdCounts).forEach(userId => {
      if (userIdCounts[userId] > 1) {
        invalidRows.push(`중복된 아이디 발견: ${userId}`);
      }
    });

    if (invalidRows.length > 0) {
      setValidationErrors(invalidRows);
      setValidationSuccess(null);
    } else {
      setValidationErrors([]);
      setValidationSuccess(
        '파일이 성공적으로 업로드되었으며 유효성 검사를 통과했습니다.'
      );
    }

    const formattedData = data.map((row, index) => ({
      id: Date.now() + index,
      group: row['그룹'] || '',
      name: row['이름'] || '',
      phoneNum: row['휴대전화'] || '',
      userId: row['아이디'] || '',
    }));

    setInputs(formattedData);
  });
  1. 각 행을 돌면서 이름, 아이디(+ 중복 확인), 휴대전화가 올바르게 입력되었는지 확인하고, 잘못된 데이터가 있으면 에러 리스트에 추가
  2. 성공 시 유효성 검사 성공 메시지 + inputs state 에 삽입

엑셀 파일 읽기

const readExcelFile = (file: File) => {
  const reader = new FileReader();
  reader.onload = event => {
    const data = event.target?.result;
    const workbook = XLSX.read(data, { type: 'binary' });
    const sheetName = workbook.SheetNames[0];
    const sheet = workbook.Sheets[sheetName];
    const jsonData = XLSX.utils.sheet_to_json<Record<string, string>>(sheet);
    validateFileData(jsonData);
  };
  reader.readAsArrayBuffer(file);
};

readExcelFile: 파일을 FileReader를 통해 읽고, XLSX 라이브러리를 사용하여 엑셀 데이터를 JSON 형식으로 변환 → 변환된 데이터는 validateFileData 함수로 유효성 검사를 진행

드래그 앤 드롭 및 파일 업로드

  const handleDragEnter = (event: React.DragEvent) => {
    event.preventDefault();
    event.stopPropagation();
    setActive(true);
  };
  const handleDragLeave = (event: React.DragEvent) => {
    event.preventDefault();
    event.stopPropagation();
    setActive(false);
  };
  const handleDragOver = (event: React.DragEvent) => {
    event.preventDefault();
    event.stopPropagation();
  };
  const handleDrop = (event: React.DragEvent) => {
    event.preventDefault();
    event.stopPropagation();
    setActive(false);
    const file = event.dataTransfer.files[0];
    setFileInfo(file);
  };

드래그 앤 드롭 이벤트 핸들러: 파일을 업로드 영역에 드롭하면 파일 정보를 설정

  const handleUpload = ({ target }: React.ChangeEvent<HTMLInputElement>) => {
    if (target.files) {
      const file = target.files[0];
      setFileInfo(file);
    }
  };

파일 선택 핸들러: 사용자가 파일을 직접 선택했을 때 파일 정보를 설정

  const handleDelete = (event: React.MouseEvent<HTMLButtonElement>) => {
    event.preventDefault();
    if (fileInputRef.current) {
      fileInputRef.current.value = '';
    }
    setUploadedInfo(null);
    setValidationErrors([]);
    setValidationSuccess(null);
    setInputs([{ id: Date.now(), group: '', name: '', userId: '', phoneNum: '' }]);
  };

파일 삭제 핸들러: 파일을 삭제하면 파일 관련 상태들을 초기화

엑셀 드래그 앤 드롭 UI

<div className={cx('file_upload')}>
     <label
       className={cx('preview', { active: isActive })}
       onDragEnter={handleDragEnter}
       onDragOver={handleDragOver}
       onDragLeave={handleDragLeave}
       onDrop={handleDrop}>
       <input
         type='file'
         className={styles.file}
         onChange={handleUpload}
         ref={fileInputRef}
       />
       {uploadedInfo ? (
         <FileInfo uploadedInfo={uploadedInfo} handleDelete={handleDelete} />
	       ) : (
         <>
           <img src={IconFileUpload} alt='파일 업로드 구역' />
           <p>클릭 또는 드래그하여 파일을 첨부해주세요</p>
         </>
        )}
     </label>
  </div>

+ UI 내에 테이블 컴포넌트

const handleDeleteRow = (index: number) => {
    const updatedData = inputs.filter((_, i) => i !== index);
    setInputs(
      updatedData.length === 0
        ? [{ id: Date.now(), group: '', name: '', userId: '', phoneNum: '' }]
        : updatedData
    );
    if (setValidationSuccess && setUploadedInfo && updatedData.length === 0) {
      setUploadedInfo(null);
      setValidationErrors([]);
      setValidationSuccess(null);
    }
  };

inputs 행이 모두 삭제되면 엑셀 파일 업로드 된 것도 초기화

생각보다 간단해서 놀람

profile
일이 재밌게 진행 되겠는걸?

0개의 댓글