Component
PhotoField
대표 사진·추가·삭제·업로드 중 상태를 조합하는 사진 필드입니다.
Playground
옵션을 조절하며 화면과 사용 코드를 함께 확인하세요.
PC 1280px
Controls
사진당 최대 MB · 비우면 제한 없음
Code
import { useState, useRef, useEffect } from 'react'
import { type PhotoFieldItem, PhotoField } from '@amineslab/ui'
function optionalLimit(value: string) {
return value.trim() && Number.isFinite(Number(value)) ? Math.max(0, Number(value)) : undefined;
}
function PhotoFieldExample({ uploadingCount = 0, disabled = false, invalid = false, variant = 'filled', opaque = false, accept = 'image/*', maxSize = '', placeholder = '사진 추가', max, showCount, showPrimaryBadge, }: {
uploadingCount?: number;
disabled?: boolean;
invalid?: boolean;
variant?: string;
accept?: string;
maxSize?: string;
placeholder?: string;
opaque?: boolean;
max: number;
showCount: boolean;
showPrimaryBadge: boolean;
}) {
const [photos, setPhotos] = useState<PhotoFieldItem[]>([]);
const [error, setError] = useState('');
const urls = useRef(new Set<string>());
useEffect(() => () => {
for (const url of urls.current)
URL.revokeObjectURL(url);
urls.current.clear();
}, []);
return (<PhotoField uploadingCount={uploadingCount} disabled={disabled} invalid={invalid} variant={variant === 'outline' ? 'outline' : 'filled'} opaque={opaque} accept={accept} maxSize={optionalLimit(maxSize)} placeholder={placeholder} onError={(errors) => setError(errors[0]?.message ?? '')} label="사진" items={photos} max={max} showCount={showCount} showPrimaryBadge={showPrimaryBadge} onAdd={(files) => setPhotos((current) => [
...current,
...files.map((file) => {
const url = URL.createObjectURL(file);
urls.current.add(url);
return { key: url, url };
}),
])} onRemove={(key) => {
setPhotos((current) => current.filter((photo) => photo.key !== key));
URL.revokeObjectURL(key);
urls.current.delete(key);
}} hint={error ? (<span role="alert" className="text-danger-700">
{error}
</span>) : ('첫 번째 사진이 대표 사진으로 사용됩니다.')}/>);
}
export default function ExamplePreview() {
return (<PhotoFieldExample {...({"uploadingCount":0,"disabled":false,"invalid":false,"variant":"filled","opaque":false,"accept":"image/*","maxSize":"","placeholder":"사진 추가","max":5,"showCount":true,"showPrimaryBadge":true} as const)}/>)
}Props
@amineslab/ui에서 PhotoField를 가져옵니다.
import { PhotoField } from '@amineslab/ui'PhotoField
PhotoGrid와 FilePicker를 사진 편집 계약으로 조합합니다.
| Name | Type | Default | Description |
|---|---|---|---|
items* | PhotoFieldItem[] | - | key와 url을 가진 사진 목록입니다. |
onAdd* | (files: File[]) => void | - | 추가된 파일을 업로드하거나 목록에 반영합니다. |
onRemove* | (key: string) => void | - | 사진 삭제를 요청합니다. |
max | number | 5 | 사진과 업로드 중 항목을 합친 최대 개수입니다. |
maxSize | number | - | 사진당 최대 용량(MB)입니다. 생략하면 제한하지 않습니다. |
onError | (errors: FileError[]) => void | - | 형식·용량·개수 제한 오류입니다. |
placeholder | string | '사진 추가' | 사진 추가 타일 문구입니다. |
uploadingCount | number | 0 | 업로드 중인 사진 수와 live 상태입니다. |
showPrimaryBadge / showCount | boolean | true | 대표 배지와 현재/최대 개수를 표시합니다. |
accept / disabled / invalid | string / boolean / boolean | - | chooser와 상태 정책입니다. |
invalid | boolean | false | 오류 테두리를 표시하며 focus·open 상태에서도 유지합니다. aria-invalid도 함께 전달합니다. |
variant | 'filled' | 'outline' | filled | 공통 필드 표면 표현입니다. focus·hover는 중립 elevation을 사용합니다. |
opaque | boolean | false | elevation 레이어 아래 불투명한 테마 바탕을 추가합니다. hover·focus·invalid 표현은 유지합니다. |
Examples
자주 쓰는 조합을 살펴보고, 필요한 예시의 코드를 펼쳐 확인하세요.
대표 사진과 업로드 수명주기
첫 항목은 대표 배지로 표시되고, 업로드 중에는 추가 타일과 live 상태가 바뀝니다.
import { useState, useRef, useEffect } from 'react'
import { type PhotoFieldItem, PhotoField } from '@amineslab/ui'
function optionalLimit(value: string) {
return value.trim() && Number.isFinite(Number(value)) ? Math.max(0, Number(value)) : undefined;
}
function PhotoFieldExample({ uploadingCount = 0, disabled = false, invalid = false, variant = 'filled', opaque = false, accept = 'image/*', maxSize = '', placeholder = '사진 추가', max, showCount, showPrimaryBadge, }: {
uploadingCount?: number;
disabled?: boolean;
invalid?: boolean;
variant?: string;
accept?: string;
maxSize?: string;
placeholder?: string;
opaque?: boolean;
max: number;
showCount: boolean;
showPrimaryBadge: boolean;
}) {
const [photos, setPhotos] = useState<PhotoFieldItem[]>([]);
const [error, setError] = useState('');
const urls = useRef(new Set<string>());
useEffect(() => () => {
for (const url of urls.current)
URL.revokeObjectURL(url);
urls.current.clear();
}, []);
return (<PhotoField uploadingCount={uploadingCount} disabled={disabled} invalid={invalid} variant={variant === 'outline' ? 'outline' : 'filled'} opaque={opaque} accept={accept} maxSize={optionalLimit(maxSize)} placeholder={placeholder} onError={(errors) => setError(errors[0]?.message ?? '')} label="사진" items={photos} max={max} showCount={showCount} showPrimaryBadge={showPrimaryBadge} onAdd={(files) => setPhotos((current) => [
...current,
...files.map((file) => {
const url = URL.createObjectURL(file);
urls.current.add(url);
return { key: url, url };
}),
])} onRemove={(key) => {
setPhotos((current) => current.filter((photo) => photo.key !== key));
URL.revokeObjectURL(key);
urls.current.delete(key);
}} hint={error ? (<span role="alert" className="text-danger-700">
{error}
</span>) : ('첫 번째 사진이 대표 사진으로 사용됩니다.')}/>);
}
export default function ExamplePreview() {
return (<PhotoFieldExample max={5} showCount showPrimaryBadge/>)
}사용 기준
권장하는 사용
- 파일 업로드 API와 목록 수명주기는 부모가 소유합니다.
- 첫 항목은 대표 사진이며, 모바일에서는 삭제 버튼의 터치 영역을 44px로 확장합니다.
피해야 할 사용
- 겉모양만으로 사용을 결정하지 마세요.
- 의미와 키보드 동작, 모바일에서의 흐름을 함께 확인하세요.
접근성
화면에 맞는 이름을 제공하고, 키보드만으로 작업을 마칠 수 있는지 확인하세요.
Enter / Space: 사진 추가 타일과 삭제 버튼을 실행합니다.