업로드
업로드 (Upload)
파일을 웹페이지나 업로드 도구를 통해 원격 서버로 게시하는 과정을 지원하는 컴포넌트입니다. 클릭, 드래그앤드롭 등 여러 방식으로 쓸 수 있어요.
출처: 문서
본문
언제 사용하나요 (When To Use)
업로드는 정보(웹페이지, 텍스트, 사진, 동영상 등)를 웹페이지나 업로드 도구를 통해 원격 서버에 게시하는 과정입니다.
- 파일을 하나 또는 여러 개 업로드해야 할 때
- 업로드 과정을 보여줘야 할 때
- 드래그 앤 드롭으로 파일을 업로드해야 할 때
예제 (Examples)
클릭하여 업로드 (Upload by clicking)
클래식 모드입니다. 업로드 버튼을 클릭하면 파일 선택 대화상자가 나타납니다.
import React from 'react';
import { UploadOutlined } from '@ant-design/icons';
import type { UploadProps } from 'antd';
import { Button, message, Upload } from 'antd';
const App: React.FC = () => {
const [messageApi, contextHolder] = message.useMessage();
const props: UploadProps = {
name: 'file',
action: 'https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload',
headers: {
authorization: 'authorization-text',
},
onChange(info) {
if (info.file.status !== 'uploading') {
console.log(info.file, info.fileList);
}
if (info.file.status === 'done') {
messageApi.success(`${info.file.name} file uploaded successfully`);
} else if (info.file.status === 'error') {
messageApi.error(`${info.file.name} file upload failed.`);
}
},
};
return (
<>
{contextHolder}
<Upload {...props}>
<Button icon={<UploadOutlined />}>Click to Upload</Button>
</Upload>
</>
);
};
export default App;
아바타 (Avatar)
클릭해 사용자 아바타를 업로드하고, beforeUpload로 사진의 크기와 형식을 검증합니다.
beforeUpload함수의 반환값은 비동기 검증을 위한 Promise일 수 있습니다. demo
import React, { useState } from 'react';
import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
import { Flex, message, Upload } from 'antd';
import type { GetProp, UploadProps } from 'antd';
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
const getBase64 = (img: FileType, callback: (url: string) => void) => {
const reader = new FileReader();
reader.addEventListener('load', () => callback(reader.result as string));
reader.readAsDataURL(img);
};
const App: React.FC = () => {
const [messageApi, contextHolder] = message.useMessage();
const [loading, setLoading] = useState(false);
const [imageUrl, setImageUrl] = useState<string>();
const beforeUpload = (file: FileType) => {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
if (!isJpgOrPng) {
messageApi.error('You can only upload JPG/PNG file!');
}
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
messageApi.error('Image must smaller than 2MB!');
}
return isJpgOrPng && isLt2M;
};
const handleChange: UploadProps['onChange'] = (info) => {
if (info.file.status === 'uploading') {
setLoading(true);
return;
}
if (info.file.status === 'done') {
// Get this url from response in real world.
getBase64(info.file.originFileObj as FileType, (url) => {
setLoading(false);
setImageUrl(url);
});
}
};
const uploadButton = (
<button style={{ border: 0, background: 'none' }} type="button">
{loading ? <LoadingOutlined /> : <PlusOutlined />}
<div style={{ marginTop: 8 }}>Upload</div>
</button>
);
return (
<>
{contextHolder}
<Flex gap="medium" wrap>
<Upload
name="avatar"
listType="picture-card"
className="avatar-uploader"
showUploadList={false}
action="https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload"
beforeUpload={beforeUpload}
onChange={handleChange}
>
{imageUrl ? (
<img draggable={false} src={imageUrl} alt="avatar" style={{ width: '100%' }} />
) : (
uploadButton
)}
</Upload>
<Upload
name="avatar"
listType="picture-circle"
className="avatar-uploader"
showUploadList={false}
action="https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload"
beforeUpload={beforeUpload}
onChange={handleChange}
>
{imageUrl ? (
<img draggable={false} src={imageUrl} alt="avatar" style={{ width: '100%' }} />
) : (
uploadButton
)}
</Upload>
</Flex>
</>
);
};
export default App;
기본 파일 (Default Files)
페이지 초기화 시 defaultFileList를 업로드된 파일로 사용합니다.
import React from 'react';
import { UploadOutlined } from '@ant-design/icons';
import type { UploadProps } from 'antd';
import { Button, Upload } from 'antd';
const props: UploadProps = {
action: 'https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload',
onChange({ file, fileList }) {
if (file.status !== 'uploading') {
console.log(file, fileList);
}
},
defaultFileList: [
{
uid: '1',
name: 'xxx.png',
status: 'uploading',
url: 'http://www.baidu.com/xxx.png',
percent: 33,
},
{
uid: '2',
name: 'yyy.png',
status: 'done',
url: 'http://www.baidu.com/yyy.png',
},
{
uid: '3',
name: 'zzz.png',
status: 'error',
response: 'Server Error 500', // custom error message to show
url: 'http://www.baidu.com/zzz.png',
},
],
};
const App: React.FC = () => (
<Upload {...props}>
<Button icon={<UploadOutlined />}>Upload</Button>
</Upload>
);
export default App;
사진 벽 (Pictures Wall)
사용자가 사진을 업로드하면 썸네일이 리스트에 표시됩니다. 개수가 한도에 도달하면 업로드 버튼이 사라집니다.
import React, { useState } from 'react';
import { PlusOutlined } from '@ant-design/icons';
import { Image, Upload } from 'antd';
import type { GetProp, UploadFile, UploadProps } from 'antd';
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
const getBase64 = (file: FileType): Promise<string> =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});
const App: React.FC = () => {
const [previewOpen, setPreviewOpen] = useState(false);
const [previewImage, setPreviewImage] = useState('');
const [fileList, setFileList] = useState<UploadFile[]>([
{
uid: '-1',
name: 'image.png',
status: 'done',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
{
uid: '-2',
name: 'image.png',
status: 'done',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
{
uid: '-3',
name: 'image.png',
status: 'done',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
{
uid: '-4',
name: 'image.png',
status: 'done',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
{
uid: '-xxx',
percent: 50,
name: 'image.png',
status: 'uploading',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
{
uid: '-5',
name: 'image.png',
status: 'error',
},
]);
const handlePreview = async (file: UploadFile) => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj as FileType);
}
setPreviewImage(file.url || (file.preview as string));
setPreviewOpen(true);
};
const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) =>
setFileList(newFileList);
const uploadButton = (
<button style={{ border: 0, background: 'none' }} type="button">
<PlusOutlined />
<div style={{ marginTop: 8 }}>Upload</div>
</button>
);
return (
<>
<Upload
action="https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload"
listType="picture-card"
fileList={fileList}
onPreview={handlePreview}
onChange={handleChange}
>
{fileList.length >= 8 ? null : uploadButton}
</Upload>
{previewImage && (
<Image
styles={{ root: { display: 'none' } }}
preview={{
open: previewOpen,
onOpenChange: (open) => setPreviewOpen(open),
afterOpenChange: (open) => !open && setPreviewImage(''),
}}
src={previewImage}
/>
)}
</>
);
};
export default App;
picture-circle 타입의 사진 (Pictures with picture-circle type)
picture-card의 대체 표시 방식입니다.
import React, { useState } from 'react';
import { PlusOutlined } from '@ant-design/icons';
import { Image, Upload } from 'antd';
import type { GetProp, UploadFile, UploadProps } from 'antd';
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
const getBase64 = (file: FileType): Promise<string> =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});
const App: React.FC = () => {
const [previewOpen, setPreviewOpen] = useState(false);
const [previewImage, setPreviewImage] = useState('');
const [fileList, setFileList] = useState<UploadFile[]>([
{
uid: '-1',
name: 'image.png',
status: 'done',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
{
uid: '-xxx',
percent: 50,
name: 'image.png',
status: 'uploading',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
{
uid: '-5',
name: 'image.png',
status: 'error',
},
]);
const handlePreview = async (file: UploadFile) => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj as FileType);
}
setPreviewImage(file.url || (file.preview as string));
setPreviewOpen(true);
};
const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) =>
setFileList(newFileList);
const uploadButton = (
<button style={{ border: 0, background: 'none' }} type="button">
<PlusOutlined />
<div style={{ marginTop: 8 }}>Upload</div>
</button>
);
return (
<>
<Upload
action="https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload"
listType="picture-circle"
fileList={fileList}
onPreview={handlePreview}
onChange={handleChange}
>
{fileList.length >= 8 ? null : uploadButton}
</Upload>
{previewImage && (
<Image
styles={{ root: { display: 'none' } }}
preview={{
open: previewOpen,
onOpenChange: (open) => setPreviewOpen(open),
afterOpenChange: (open) => !open && setPreviewImage(''),
}}
src={previewImage}
/>
)}
</>
);
};
export default App;
파일 리스트 완전 제어 (Complete control over file list)
fileList를 구성해 파일 리스트를 완전히 제어할 수 있습니다. 온갖 커스텀 기능을 구현할 수 있습니다. 다음은 두 가지 상황을 보여줍니다:
-
업로드되는 파일 수를 제한합니다.
-
응답에서 읽어 파일 링크를 표시합니다.
import React, { useState } from 'react';
import { UploadOutlined } from '@ant-design/icons';
import type { UploadFile, UploadProps } from 'antd';
import { Button, Upload } from 'antd';
const App: React.FC = () => {
const [fileList, setFileList] = useState<UploadFile[]>([
{
uid: '-1',
name: 'xxx.png',
status: 'done',
url: 'http://www.baidu.com/xxx.png',
},
]);
const handleChange: UploadProps['onChange'] = (info) => {
let newFileList = [...info.fileList];
// 1. Limit the number of uploaded files
// Only to show two recent uploaded files, and old ones will be replaced by the new
newFileList = newFileList.slice(-2);
// 2. Read from response and show file link
newFileList = newFileList.map((file) => {
if (file.response) {
// Component will show file.url as link
file.url = file.response.url;
}
return file;
});
setFileList(newFileList);
};
const props = {
action: 'https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload',
onChange: handleChange,
multiple: true,
};
return (
<Upload {...props} fileList={fileList}>
<Button icon={<UploadOutlined />}>Upload</Button>
</Upload>
);
};
export default App;
드래그 앤 드롭 (Drag and Drop)
파일을 특정 영역으로 드래그해 업로드할 수 있습니다. 또는 선택 방식으로도 업로드할 수 있습니다.
현대 브라우저에서는 input에 multiple 속성을 주면 여러 파일을 한 번에 업로드할 수 있습니다.
import React from 'react';
import { InboxOutlined } from '@ant-design/icons';
import type { UploadProps } from 'antd';
import { message, Upload } from 'antd';
const { Dragger } = Upload;
const App: React.FC = () => {
const [messageApi, contextHolder] = message.useMessage();
const props: UploadProps = {
name: 'file',
multiple: true,
action: 'https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload',
onChange(info) {
const { status } = info.file;
if (status !== 'uploading') {
console.log(info.file, info.fileList);
}
if (status === 'done') {
messageApi.success(`${info.file.name} file uploaded successfully.`);
}
if (status === 'error') {
messageApi.error(`${info.file.name} file upload failed.`);
}
},
onDrop(e) {
console.log('Dropped files', e.dataTransfer.files);
},
};
return (
<>
{contextHolder}
<Dragger {...props}>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text">Click or drag file to this area to upload</p>
<p className="ant-upload-hint">
Support for a single or bulk upload. Strictly prohibited from uploading company data or
other banned files.
</p>
</Dragger>
</>
);
};
export default App;
붙여넣기 (Paste)
파일을 복사해 페이지 어디에나 붙여넣어 업로드합니다.
import React from 'react';
import { UploadOutlined } from '@ant-design/icons';
import type { UploadProps } from 'antd';
import { Button, message, Upload } from 'antd';
const App: React.FC = () => {
const [messageApi, contextHolder] = message.useMessage();
const props: UploadProps = {
name: 'file',
pastable: true,
action: 'https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload',
headers: {
authorization: 'authorization-text',
},
onChange(info) {
if (info.file.status !== 'uploading') {
console.log(info.file, info.fileList);
}
if (info.file.status === 'done') {
messageApi.success(`${info.file.name} file uploaded successfully`);
} else if (info.file.status === 'error') {
messageApi.error(`${info.file.name} file upload failed.`);
}
},
};
return (
<>
{contextHolder}
<Upload {...props}>
<Button icon={<UploadOutlined />}>Paste or click to upload</Button>
</Upload>
</>
);
};
export default App;
디렉토리 업로드 (Upload directory)
전체 디렉토리를 선택해 업로드할 수 있습니다. Safari에서 폴더 업로드 시에도 파일을 선택할 수 있나요?
import React from 'react';
import { UploadOutlined } from '@ant-design/icons';
import { Button, Upload } from 'antd';
const App: React.FC = () => (
<Upload action="https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload" directory>
<Button icon={<UploadOutlined />}>Upload Directory</Button>
</Upload>
);
export default App;
수동 업로드 (Upload manually)
beforeUpload가 false를 반환한 뒤 파일을 수동으로 업로드합니다.
import React, { useState } from 'react';
import { UploadOutlined } from '@ant-design/icons';
import { Button, message, Upload } from 'antd';
import type { GetProp, UploadFile, UploadProps } from 'antd';
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
const App: React.FC = () => {
const [messageApi, contextHolder] = message.useMessage();
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [uploading, setUploading] = useState(false);
const handleUpload = () => {
const formData = new FormData();
fileList.forEach((file) => {
formData.append('files[]', file as FileType);
});
setUploading(true);
// You can use any AJAX library you like
fetch('https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload', {
method: 'POST',
body: formData,
})
.then((res) => res.json())
.then(() => {
setFileList([]);
messageApi.success('upload successfully.');
})
.catch(() => {
messageApi.error('upload failed.');
})
.finally(() => {
setUploading(false);
});
};
const props: UploadProps = {
onRemove: (file) => {
const index = fileList.indexOf(file);
const newFileList = fileList.slice();
newFileList.splice(index, 1);
setFileList(newFileList);
},
beforeUpload: (file) => {
setFileList([...fileList, file]);
return false;
},
fileList,
};
return (
<>
{contextHolder}
<Upload {...props}>
<Button icon={<UploadOutlined />}>Select File</Button>
</Upload>
<Button
type="primary"
onClick={handleUpload}
disabled={fileList.length === 0}
loading={uploading}
style={{ marginTop: 16 }}
>
{uploading ? 'Uploading' : 'Start Upload'}
</Button>
</>
);
};
export default App;
png 파일만 업로드 (Upload png file only)
beforeUpload는 false를 반환하거나 promise를 거부할 때만 업로드 동작을 막으며, 막힌 파일은 여전히 파일 리스트에 표시됩니다. UPLOAD.LIST_IGNORE를 반환해 막힌 파일을 리스트에서 제외하는 예제가 여기 있습니다.
import React from 'react';
import { UploadOutlined } from '@ant-design/icons';
import type { UploadProps } from 'antd';
import { Button, message, Upload } from 'antd';
const App: React.FC = () => {
const [messageApi, contextHolder] = message.useMessage();
const props: UploadProps = {
beforeUpload: (file) => {
const isPNG = file.type === 'image/png';
if (!isPNG) {
messageApi.error(`${file.name} is not a png file`);
}
return isPNG || Upload.LIST_IGNORE;
},
onChange: (info) => {
console.log(info.fileList);
},
};
return (
<>
{contextHolder}
<Upload {...props}>
<Button icon={<UploadOutlined />}>Upload png only</Button>
</Upload>
</>
);
};
export default App;
리스트 스타일의 사진 (Pictures with list style)
업로드된 파일이 사진이면 썸네일을 표시할 수 있습니다. IE8/9는 로컬 썸네일 표시를 지원하지 않습니다. 대신 thumbUrl을 사용하세요.
import React from 'react';
import { UploadOutlined } from '@ant-design/icons';
import { Button, Upload } from 'antd';
import type { UploadFile } from 'antd';
const fileList: UploadFile[] = [
{
uid: '0',
name: 'xxx.png',
status: 'uploading',
percent: 33,
},
{
uid: '-1',
name: 'yyy.png',
status: 'done',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
{
uid: '-2',
name: 'zzz.png',
status: 'error',
},
];
const App: React.FC = () => (
<Upload
action="https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload"
listType="picture"
defaultFileList={fileList}
>
<Button type="primary" icon={<UploadOutlined />}>
Upload
</Button>
</Upload>
);
export default App;
미리보기 파일 커스터마이즈 (Customize preview file)
로컬 미리보기를 커스터마이즈합니다. 비디오 같은 비이미지 형식 파일도 처리할 수 있습니다.
import React from 'react';
import { UploadOutlined } from '@ant-design/icons';
import type { UploadProps } from 'antd';
import { Button, Upload } from 'antd';
const props: UploadProps = {
action: '//jsonplaceholder.typicode.com/posts/',
listType: 'picture',
previewFile(file) {
console.log('Your upload file:', file);
// Your process logic. Here we just mock to the same file
return fetch('https://next.json-generator.com/api/json/get/4ytyBoLK8', {
method: 'POST',
body: file,
})
.then((res) => res.json())
.then(({ thumbnail }) => thumbnail);
},
};
const App: React.FC = () => (
<Upload {...props}>
<Button icon={<UploadOutlined />}>Upload</Button>
</Upload>
);
export default App;
최대 개수 (Max Count)
maxCount로 파일 수를 제한합니다. maxCount가 1이면 현재 파일이 대체됩니다.
import React from 'react';
import { UploadOutlined } from '@ant-design/icons';
import { Button, Space, Upload } from 'antd';
const App: React.FC = () => (
<Space vertical style={{ width: '100%' }} size="large">
<Upload
action="https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload"
listType="picture"
maxCount={1}
>
<Button icon={<UploadOutlined />}>Upload (Max: 1)</Button>
</Upload>
<Upload
action="https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload"
listType="picture"
maxCount={3}
multiple
>
<Button icon={<UploadOutlined />}>Upload (Max: 3)</Button>
</Upload>
</Space>
);
export default App;
요청 전 파일 변환 (Transform file before request)
워터마크 추가처럼 요청 전 파일을 변환하려면 beforeUpload를 사용합니다.
import React from 'react';
import { UploadOutlined } from '@ant-design/icons';
import type { UploadProps } from 'antd';
import { Button, Upload } from 'antd';
const props: UploadProps = {
action: 'https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload',
listType: 'picture',
beforeUpload(file) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
const img = document.createElement('img');
img.src = reader.result as string;
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d')!;
ctx.drawImage(img, 0, 0);
ctx.fillStyle = 'red';
ctx.textBaseline = 'middle';
ctx.font = '33px Arial';
ctx.fillText('Ant Design', 20, 20);
canvas.toBlob((result) => resolve(result as Blob));
};
};
});
},
};
const App: React.FC = () => (
<Upload {...props}>
<Button icon={<UploadOutlined />}>Upload</Button>
</Upload>
);
export default App;
Aliyun OSS
Aliyun OSS 업로드 예제를 사용합니다.
import React, { useEffect, useState } from 'react';
import { UploadOutlined } from '@ant-design/icons';
import type { UploadFile, UploadProps } from 'antd';
import { App, Button, Form, Upload } from 'antd';
interface OSSDataType {
dir: string;
expire: string;
host: string;
accessId: string;
policy: string;
signature: string;
}
interface AliyunOSSUploadProps {
value?: UploadFile[];
onChange?: (fileList: UploadFile[]) => void;
}
// Mock get OSS api
// https://help.aliyun.com/document_detail/31988.html
const mockOSSData = () => {
const mockData = {
dir: 'user-dir/',
expire: '1577811661',
host: 'https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload',
accessId: 'c2hhb2RhaG9uZw==',
policy: 'eGl4aWhhaGFrdWt1ZGFkYQ==',
signature: 'ZGFob25nc2hhbw==',
};
return Promise.resolve(mockData);
};
const AliyunOSSUpload: React.FC<Readonly<AliyunOSSUploadProps>> = ({ value, onChange }) => {
const { message } = App.useApp();
const [OSSData, setOSSData] = useState<OSSDataType>();
const init = async () => {
try {
const result = await mockOSSData();
setOSSData(result);
} catch (err) {
if (err instanceof Error) {
message.error(err.message);
}
}
};
useEffect(() => {
init();
}, []);
const handleChange: UploadProps['onChange'] = ({ fileList }) => {
console.log('Aliyun OSS:', fileList);
onChange?.([...fileList]);
};
const onRemove = (file: UploadFile) => {
const files = (value || []).filter((v) => v.url !== file.url);
onChange?.(files);
};
const getExtraData: UploadProps['data'] = (file) => ({
key: file.url,
OSSAccessKeyId: OSSData?.accessId,
policy: OSSData?.policy,
Signature: OSSData?.signature,
});
const beforeUpload: UploadProps['beforeUpload'] = async (file) => {
if (!OSSData) {
return false;
}
const expire = Number(OSSData.expire) * 1000;
if (expire < Date.now()) {
await init();
}
const suffix = file.name.slice(file.name.lastIndexOf('.'));
const filename = Date.now() + suffix;
// @ts-ignore
file.url = OSSData.dir + filename;
return file;
};
const uploadProps: UploadProps = {
name: 'file',
fileList: value,
action: OSSData?.host,
onChange: handleChange,
onRemove,
data: getExtraData,
beforeUpload,
};
return (
<Upload {...uploadProps}>
<Button icon={<UploadOutlined />}>Click to Upload</Button>
</Upload>
);
};
const Demo: React.FC = () => (
<Form labelCol={{ span: 4 }}>
<Form.Item label="Photos" name="photos">
<AliyunOSSUpload />
</Form.Item>
</Form>
);
export default Demo;
커스텀 액션 아이콘과 추가 정보 (Custom action icon and extra info)
showUploadList로 파일의 커스텀 액션 아이콘과 추가 정보를 표시합니다.
import React from 'react';
import { StarOutlined, UploadOutlined } from '@ant-design/icons';
import type { UploadProps } from 'antd';
import { Button, Upload } from 'antd';
const props: UploadProps = {
action: 'https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload',
onChange({ file, fileList }) {
if (file.status !== 'uploading') {
console.log(file, fileList);
}
},
defaultFileList: [
{
uid: '1',
name: 'xxx.png',
size: 1234567,
status: 'done',
response: 'Server Error 500', // custom error message to show
url: 'http://www.baidu.com/xxx.png',
},
{
uid: '2',
name: 'yyy.png',
size: 1234567,
status: 'done',
url: 'http://www.baidu.com/yyy.png',
},
{
uid: '3',
name: 'zzz.png',
size: 1234567,
status: 'error',
response: 'Server Error 500', // custom error message to show
url: 'http://www.baidu.com/zzz.png',
},
],
showUploadList: {
extra: ({ size = 0 }) => (
<span style={{ color: '#cccccc' }}>({(size / 1024 / 1024).toFixed(2)}MB)</span>
),
showDownloadIcon: true,
downloadIcon: 'Download',
showRemoveIcon: true,
removeIcon: <StarOutlined onClick={(e) => console.log(e, 'custom removeIcon event')} />,
},
};
const App: React.FC = () => (
<Upload {...props}>
<Button icon={<UploadOutlined />}>Upload</Button>
</Upload>
);
export default App;
uploadList의 드래그 정렬 (Drag sorting of uploadList)
itemRender를 사용해 dnd-kit과 업로드를 통합해 uploadList의 드래그 정렬을 구현할 수 있습니다.
import React, { useState } from 'react';
import { UploadOutlined } from '@ant-design/icons';
import type { DragEndEvent } from '@dnd-kit/core';
import { DndContext, PointerSensor, useSensor } from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Button, Upload } from 'antd';
import type { UploadFile, UploadProps } from 'antd';
import { createStyles } from 'antd-style';
const useStyles = createStyles((props) => {
const { css } = props;
return {
isDragging: css`
pointer-events: none;
a {
pointer-events: none;
}
`,
};
});
interface DraggableUploadListItemProps {
originNode: React.ReactElement<any, string | React.JSXElementConstructor<any>>;
file: UploadFile<any>;
}
const DraggableUploadListItem: React.FC<DraggableUploadListItemProps> = (props) => {
const { styles } = useStyles();
const { originNode, file } = props;
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: file.uid,
});
const style: React.CSSProperties = {
transform: CSS.Translate.toString(transform),
transition,
cursor: 'move',
};
return (
<div
ref={setNodeRef}
style={style}
// prevent preview event when drag end
className={isDragging ? styles.isDragging : undefined}
{...attributes}
{...listeners}
>
{/* hide error tooltip when dragging */}
{file.status === 'error' && isDragging ? originNode.props.children : originNode}
</div>
);
};
const App: React.FC = () => {
const [fileList, setFileList] = useState<UploadFile[]>([
{
uid: '-1',
name: 'image1.png',
status: 'done',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
{
uid: '-2',
name: 'image2.png',
status: 'done',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
{
uid: '-3',
name: 'image3.png',
status: 'done',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
{
uid: '-4',
name: 'image4.png',
status: 'done',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
{
uid: '-5',
name: 'image.png',
status: 'error',
},
]);
const sensor = useSensor(PointerSensor, {
activationConstraint: { distance: 10 },
});
const onDragEnd = ({ active, over }: DragEndEvent) => {
if (active.id !== over?.id) {
setFileList((prev) => {
const activeIndex = prev.findIndex((i) => i.uid === active.id);
const overIndex = prev.findIndex((i) => i.uid === over?.id);
return arrayMove(prev, activeIndex, overIndex);
});
}
};
const onChange: UploadProps['onChange'] = ({ fileList: newFileList }) => {
setFileList(newFileList);
};
return (
<DndContext sensors={[sensor]} onDragEnd={onDragEnd}>
<SortableContext items={fileList.map((i) => i.uid)} strategy={verticalListSortingStrategy}>
<Upload
action="https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload"
fileList={fileList}
onChange={onChange}
itemRender={(originNode, file) => (
<DraggableUploadListItem originNode={originNode} file={file} />
)}
>
<Button icon={<UploadOutlined />}>Click to Upload</Button>
</Upload>
</SortableContext>
</DndContext>
);
};
export default App;
업로드 전 이미지 크롭 (Crop image before uploading)
antd-img-crop을 사용해 업로드 전에 이미지를 크롭합니다.
import React, { useState } from 'react';
import { Upload } from 'antd';
import type { GetProp, UploadFile, UploadProps } from 'antd';
import ImgCrop from 'antd-img-crop';
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
const App: React.FC = () => {
const [fileList, setFileList] = useState<UploadFile[]>([
{
uid: '-1',
name: 'image.png',
status: 'done',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
]);
const onChange: UploadProps['onChange'] = ({ fileList: newFileList }) => {
setFileList(newFileList);
};
const onPreview = async (file: UploadFile) => {
let src = file.url as string;
if (!src) {
src = await new Promise((resolve) => {
const reader = new FileReader();
reader.readAsDataURL(file.originFileObj as FileType);
reader.onload = () => resolve(reader.result as string);
});
}
const image = new Image();
image.src = src;
const imgWindow = window.open(src);
imgWindow?.document.write(image.outerHTML);
};
return (
<ImgCrop rotationSlider>
<Upload
action="https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload"
listType="picture-card"
fileList={fileList}
onChange={onChange}
onPreview={onPreview}
>
{fileList.length < 5 && '+ Upload'}
</Upload>
</ImgCrop>
);
};
export default App;
진행 바 커스터마이즈 (Customize Progress Bar)
progress를 사용해 진행 바를 커스터마이즈합니다.
import React from 'react';
import { UploadOutlined } from '@ant-design/icons';
import type { UploadProps } from 'antd';
import { Button, message, Upload } from 'antd';
const App: React.FC = () => {
const [messageApi, contextHolder] = message.useMessage();
const props: UploadProps = {
name: 'file',
action: 'https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload',
headers: {
authorization: 'authorization-text',
},
onChange(info) {
if (info.file.status !== 'uploading') {
console.log(info.file, info.fileList);
}
if (info.file.status === 'done') {
messageApi.success(`${info.file.name} file uploaded successfully`);
} else if (info.file.status === 'error') {
messageApi.error(`${info.file.name} file upload failed.`);
}
},
progress: {
strokeColor: {
'0%': '#108ee9',
'100%': '#87d068',
},
strokeWidth: 3,
format: (percent) => percent && `${Number.parseFloat(percent.toFixed(2))}%`,
},
};
return (
<>
{contextHolder}
<Upload {...props}>
<Button icon={<UploadOutlined />}>Click to Upload</Button>
</Upload>
</>
);
};
export default App;
커스텀 시맨틱 DOM 스타일링 (Custom semantic dom styling)
classNames와 styles에 객체 또는 함수를 넘겨서 Upload 컴포넌트의 시맨틱 DOM 스타일을 커스터마이즈할 수 있습니다.
import React from 'react';
import { UploadOutlined } from '@ant-design/icons';
import { Button, Flex, Upload } from 'antd';
import type { GetProp, UploadProps } from 'antd';
import { createStyles } from 'antd-style';
const useStyles = createStyles(({ token }) => ({
root: {
borderRadius: token.borderRadius,
padding: token.padding,
},
}));
const stylesObject: UploadProps<any>['styles'] = {
item: {
borderRadius: 2,
backgroundColor: 'rgba(5, 5, 5, 0.06)',
height: 30,
},
trigger: {
backgroundColor: 'rgba(84, 89, 172, 0.1)',
padding: 8,
borderRadius: 4,
},
};
const stylesFn: UploadProps<any>['styles'] = (
info,
): GetProp<UploadProps<any>, 'styles', 'Return'> => {
if (info.props.multiple) {
return {
root: { border: '1px solid #5459AC' },
item: {
borderRadius: 2,
backgroundColor: 'rgba(5, 5, 5, 0.06)',
height: 30,
},
trigger: {
backgroundColor: 'rgba(84, 89, 172, 0.2)',
padding: 8,
borderRadius: 4,
},
};
}
return {};
};
const App: React.FC = () => {
const { styles: classNames } = useStyles();
const uploadProps: UploadProps<any> = {
classNames,
action: 'https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload',
onChange({ file, fileList }) {
if (file.status !== 'uploading') {
console.log(file, fileList);
}
},
defaultFileList: [
{
uid: '1',
name: 'xxx.png',
status: 'uploading',
url: 'http://www.baidu.com/xxx.png',
percent: 33,
},
{
uid: '2',
name: 'yyy.png',
status: 'done',
url: 'http://www.baidu.com/yyy.png',
},
{
uid: '3',
name: 'zzz.png',
status: 'error',
response: 'Server Error 500', // custom error message to show
url: 'http://www.baidu.com/zzz.png',
},
],
};
return (
<Flex gap="large" vertical>
<Upload {...uploadProps} styles={stylesObject}>
<Button icon={<UploadOutlined />}>Upload</Button>
</Upload>
<Upload {...uploadProps} styles={stylesFn} multiple>
<Button icon={<UploadOutlined />}>Upload</Button>
</Upload>
</Flex>
);
};
export default App;
API
Common props ref:Common props
| Property | Description | Type | Default | Version | Global Config |
|---|---|---|---|---|---|
| accept | File types that can be accepted. See input accept Attribute | string | AcceptObject | - | 6.4.0 | |
| action | Uploading URL | string | (file) => Promise<string> | - | × | |
| beforeUpload | Hook function which will be executed before uploading. Uploading will be stopped with false or a rejected Promise returned. When returned value is Upload.LIST_IGNORE, the list of files that have been uploaded will ignore it. Warning:this function is not supported in IE9 |
(file: RcFile, fileList: RcFile[]) => boolean | Promise<File> | Upload.LIST_IGNORE |
- | × | |
| customRequest | Override for the default xhr behavior allowing for additional customization and the ability to implement your own XMLHttpRequest | ( options: RequestOptions, info: { defaultRequest: (option: RequestOptions) => void; } ) => void | - | defaultRequest: 5.28.0 | 5.27.0 |
| classNames | Customize class for each semantic structure inside the component. Supports object or function. | Record<SemanticDOM, string> | (info: { props })=> Record<SemanticDOM, string> | - | 6.0.0 | |
| data | Uploading extra params or function which can return uploading extra params | object | (file) => object | Promise<object> | - | × | |
| defaultFileList | Default list of files that have been uploaded | object[] | - | × | |
| directory | Support upload whole directory (caniuse) | boolean | false | × | |
| disabled | Disable upload button. When customizing Upload children, please pass the disabled attribute to the child node at the same time to ensure the disabled rendering effect is consistent. |
boolean | false | × | |
| fileList | List of files that have been uploaded (controlled). Here is a common issue #2423 when using it | UploadFile[] | - | × | |
| headers | Set request headers, valid above IE10 | object | - | × | |
| iconRender | Custom show icon | (file: UploadFile, listType?: UploadListType) => ReactNode | - | × | |
| isImageUrl | Customize if render <img /> in thumbnail | (file: UploadFile) => boolean | (inside implementation) | × | |
| itemRender | Custom item of uploadList | (originNode: ReactElement, file: UploadFile, fileList: object[], actions: { download: function, preview: function, remove: function }) => React.ReactNode | - | 4.16.0 | × |
| listType | Built-in stylesheets, support for four types: text, picture, picture-card or picture-circle |
string | text |
picture-circle(5.2.0+) |
× |
| maxCount | Limit the number of uploaded files. Will replace current one when maxCount is 1 |
number | - | 4.10.0 | × |
| method | The http method of upload request | string | post |
× | |
| multiple | Whether to support selected multiple files. IE10+ supported. You can select multiple files with CTRL holding down while multiple is set to be true |
boolean | false | × | |
| name | The name of uploading file | string | file |
× | |
| openFileDialogOnClick | Click open file dialog | boolean | true | × | |
| pastable | Support paste file | boolean | false | 5.25.0 | × |
| previewFile | Customize preview file logic | (file: File | Blob) => Promise<dataURL: string> | - | × | |
| progress | Custom progress bar | ProgressProps (support type="line" only) |
{ strokeWidth: 2, showInfo: false } | 4.3.0 | 6.4.0 |
| showUploadList | Whether to show default upload list, could be an object to specify extra, showPreviewIcon, showRemoveIcon, showDownloadIcon, removeIcon and downloadIcon individually |
boolean | { extra?: ReactNode | (file: UploadFile) => ReactNode, showPreviewIcon?: boolean | (file: UploadFile) => boolean, showDownloadIcon?: boolean | (file: UploadFile) => boolean, showRemoveIcon?: boolean | (file: UploadFile) => boolean, previewIcon?: ReactNode | (file: UploadFile) => ReactNode, removeIcon?: ReactNode | (file: UploadFile) => ReactNode, downloadIcon?: ReactNode | (file: UploadFile) => ReactNode } | true | extra: 5.20.0, showPreviewIcon function: 5.21.0, showRemoveIcon function: 5.21.0, showDownloadIcon function: 5.21.0 |
× |
| styles | Customize inline style for each semantic structure inside the component. Supports object or function. | Record<SemanticDOM, CSSProperties> | (info: { props })=> Record<SemanticDOM, CSSProperties> | - | 6.0.0 | |
| withCredentials | The ajax upload with cookie sent | boolean | false | × | |
| onChange | A callback function, can be executed when uploading state is changing. It will trigger by every uploading phase. see onChange | function | - | × | |
| onDrop | A callback function executed when files are dragged and dropped into the upload area | (event: React.DragEvent) => void | - | 4.16.0 | × |
| onDownload | Click the method to download the file, pass the method to perform the method logic, and do not pass the default jump to the new TAB | function(file): void | (Jump to new TAB) | × | |
| onPreview | A callback function, will be executed when the file link or preview icon is clicked | function(file) | - | × | |
| onRemove | A callback function, will be executed when removing file button is clicked, remove event will be prevented when the return value is false or a Promise which resolve(false) or reject | function(file): boolean | Promise | - | × |
인터페이스 (Interface)
RcFile
File을 확장합니다.
| Property | Description | Type | Default | Version |
|---|---|---|---|---|
| uid | unique id. Will auto-generate when not provided | string | - | - |
| lastModifiedDate | A Date object indicating the date and time at which the file was last modified | date | - | - |
UploadFile
추가 props와 함께 File을 확장합니다.
| Property | Description | Type | Default | Version |
|---|---|---|---|---|
| crossOrigin | CORS settings attributes | 'anonymous' | 'use-credentials' | '' |
- | 4.20.0 |
| name | File name | string | - | - |
| percent | Upload progress percent | number | - | - |
| status | Upload status. Show different style when configured | error | done | uploading | removed |
- | - |
| thumbUrl | Thumb image url | string | - | - |
| uid | unique id. Will auto-generate when not provided | string | - | - |
| url | Download url | string | - | - |
RequestOptions {#request-options}
| Property | Description | Type | Default | Version |
|---|---|---|---|---|
| action | Uploading URL | string | - | - |
| data | Uploading extra params or function which can return uploading extra params | Record<string, unknown> | - | - |
| filename | file name | string | - | - |
| file | File object containing upload information | UploadFile | - | - |
| withCredentials | The ajax upload with cookie sent | boolean | - | - |
| headers | Set request headers, valid above IE10 | Record<string, string> | - | - |
| method | The http method of upload request | string | - | - |
| onProgress | Progress event callback | (event: object, file: UploadFile) => void | - | - |
| onError | Error callback when upload fails | (event: object, body?: object) => void | - | - |
| onSuccess | Success callback when upload completes | (body: object, fileOrXhr?: UploadFile | XMLHttpRequest) => void | - | - |
onChange
💡 이 함수는 업로드가 진행 중, 완료 또는 실패했을 때 호출됩니다.
업로드 상태가 변경되면 다음을 반환합니다:
{
file: { /* ... */ },
fileList: [ /* ... */ ],
event: { /* ... */ },
}
-
file현재 작업의 File 객체입니다.{ uid: 'uid', // unique identifier, negative is recommended, to prevent interference with internally generated id name: 'xx.png', // file name status: 'done' | 'uploading' | 'error' | 'removed', // Intercepted file by beforeUpload doesn't have a status field. response: '{"status": "success"}', // response from server linkProps: '{"download": "image"}', // additional HTML props of file link xhr: 'XMLHttpRequest{ ... }', // XMLHttpRequest Header } -
fileList현재 파일 목록입니다. -
event업로드 진행률을 포함한 서버의 응답으로, 고급 브라우저에서 지원됩니다.
AcceptObject
{
format: string;
filter?: 'native' | ((file: RcFile) => boolean);
}
파일 타입 수용 규칙을 위한 구성 객체입니다.
| Property | Description | Type | Default | Version |
|---|---|---|---|---|
| format | Accepted file types, same as native input accept attribute. Supports MIME types, file extensions, etc. See input accept Attribute | string | - | |
| filter | File filtering rule. When set to 'native', uses browser native filtering behavior; when set to a function, allows custom filtering logic. Function returns true to accept the file, false to reject |
'native' | (file: RcFile) => boolean |
- |
시맨틱 DOM (Semantic DOM)
https://ant.design/components/upload/semantic.md
디자인 토큰 (Design Token)
컴포넌트 토큰 (Component Token - Upload)
| Token Name | Description | Type | Default Value |
|---|---|---|---|
| actionsColor | Action button color | string | rgba(0,0,0,0.45) |
| pictureCardSize | Size of list items in card type (affects both picture-card and picture-circle) | number | 102 |
글로벌 토큰 (Global Token)
| Token Name | Description | Type | Default Value |
|---|---|---|---|
| borderRadiusLG | LG size border radius, used in some large border radius components, such as Card, Modal and other components. | number | |
| borderRadiusSM | SM size border radius, used in small size components, such as Button, Input, Select and other input components in small size | number | |
| colorBgMask | The background color of the mask, used to cover the content below the mask, Modal, Drawer, Image and other components use this token | string | |
| colorBorder | Default border color, used to separate different elements, such as: form separator, card separator, etc. | string | |
| colorError | Used to represent the visual elements of the operation failure, such as the error Button, error Result component, etc. | string | |
| colorFillAlter | Control the alternative background color of element. | string | |
| colorIcon | Weak action. Such as allowClear or Alert close button |
string | |
| colorPrimary | Brand color is one of the most direct visual elements to reflect the characteristics and communication of the product. After you have selected the brand color, we will automatically generate a complete color palette and assign it effective design semantics. | string | |
| colorPrimaryBorder | The stroke color under the main color gradient, used on the stroke of components such as Slider. | string | |
| colorPrimaryHover | Hover state under the main color gradient. | string | |
| colorText | Default text color which comply with W3C standards, and this color is also the darkest neutral color. | string | |
| colorTextDescription | Control the font color of text description. | string | |
| colorTextDisabled | Control the color of text in disabled state. | string | |
| colorTextHeading | Control the font color of heading. | string | |
| colorTextLightSolid | Control the highlight color of text with background color, such as the text in Primary Button components. | string | |
| controlItemBgHover | Control the background color of control component item when hovering. | string | |
| fontFamily | The font family of Ant Design prioritizes the default interface font of the system, and provides a set of alternative font libraries that are suitable for screen display to maintain the readability and readability of the font under different platforms and browsers, reflecting the friendly, stable and professional characteristics. | string | |
| fontSize | The most widely used font size in the design system, from which the text gradient will be derived. | number | |
| fontSizeHeading2 | Font size of h2 tag. | string | number | |
| fontSizeHeading3 | Font size of h3 tag. | string | number | |
| fontSizeLG | Large font size | number | |
| lineHeight | Line height of text. | number | |
| lineType | Border style of base components | string | |
| lineWidth | Border width of base components | number | |
| lineWidthFocus | Control the width of the line when the component is in focus state. | number | |
| margin | Control the margin of an element, with a medium size. | number | |
| marginXL | Control the margin of an element, with an extra-large size. | number | |
| marginXS | Control the margin of an element, with a small size. | number | |
| marginXXS | Control the margin of an element, with the smallest size. | number | |
| motionDurationMid | Motion speed, medium speed. Used for medium element animation interaction. | string | |
| motionDurationSlow | Motion speed, slow speed. Used for large element animation interaction. | string | |
| motionEaseInOut | Preset motion curve. | string | |
| motionEaseInOutCirc | Preset motion curve. | string | |
| padding | Control the padding of the element. | number | |
| paddingSM | Control the small padding of the element. | number | |
| paddingXS | Control the extra small padding of the element. | number |
FAQ
서버 측 업로드를 어떻게 구현하나요? {#faq-server-implement}
- jQuery-File-Upload에서 서버 측 업로드 인터페이스 구현 방법을 참고할 수 있습니다.
- rc-upload에 express의 mock 예제가 있습니다.
다운로드 링크를 표시하고 싶습니다. {#faq-show-download-link}
fileList의 각 항목에 url 속성을 설정해 링크 콘텐츠를 제어하세요.
customRequest를 어떻게 사용하나요? {#faq-custom-request}
https://github.com/react-component/upload#customrequest를 참고하세요.
제어되는 fileList에서, 파일이 리스트에 없을 때 onChange의 status 업데이트가 트리거되지 않는 이유는 무엇인가요? {#faq-filelist-controlled-status}
onChange는 파일이 리스트에 있을 때만 트리거되며, 리스트에서 제거된 이벤트는 무시합니다. 4.13.0 이전에는 파일이 리스트에 없어도 이벤트가 여전히 트리거되는 버그가 존재했음을 참고하세요.
onChange가 때로는 File 객체를, 때로는 { originFileObj: File }을 반환하는 이유는 무엇인가요? {#faq-on-change-return-type}
호환성 때문에 beforeUpload가 false를 반환하면 File 객체를 반환합니다. 다음 메이저 버전에서는 { originFileObj: File }로 병합됩니다. 현재 버전은 info.file.originFileObj로 원본 파일을 얻는 것과 호환됩니다. 메이저 릴리즈 전에 이를 변경할 수 있습니다.
Chrome이 가끔 업로드할 수 없는 이유는 무엇인가요? {#faq-chrome-file-picker}
Chrome 업데이트는 기본 업로드도 깨뜨릴 수 있습니다. 업로드 작업을 완료하려면 Chrome을 다시 시작하세요.
참조:
Safari에서 폴더 업로드 시 파일을 계속 선택할 수 있나요? {#faq-safari-folder-upload}
업로드 컴포넌트 내부에서 directory와 webkitdirectory 속성을 사용해 input을 제어해 폴더 선택을 구현하지만, Safari 구현에서는 파일 선택을 막지 않는 것으로 보입니다. accept 구성을 통해 이 문제를 해결할 수 있습니다. 예를 들어:
accept = {
// Do not allow selecting any files
format: `.${'n'.repeat(100)}`,
// Accept all files within the folder after folder selection
filter: () => true,
};