회사 관리자 페이지에 엑셀 파일을 업로드하고 화면에 반영하는 걸 구현해야되었는데 처음 해보는 것이였어서 기록하고자 남김!
드래그 앤 드랍과 클릭 이벤트로 파일 업로드가 가능하고, 엑셀 파일이 삭제 되거나 정보가 입력된 행이 다 삭제되면 반대의 것도 자동으로 삭제되게 구현했다.
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;
}
const requiredColumns = ['이름', '아이디', '휴대전화'];
const firstRow = data[0];
const missingColumns = requiredColumns.filter(
col => !Object.prototype.hasOwnProperty.call(firstRow, col)
);
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);
});
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: '' }]);
};
파일 삭제 핸들러: 파일을 삭제하면 파일 관련 상태들을 초기화
<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 행이 모두 삭제되면 엑셀 파일 업로드 된 것도 초기화
생각보다 간단해서 놀람