본문으로 건너뛰기
Amineslab UI

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를 사진 편집 계약으로 조합합니다.

NameTypeDefaultDescription
items*PhotoFieldItem[]-key와 url을 가진 사진 목록입니다.
onAdd*(files: File[]) => void-추가된 파일을 업로드하거나 목록에 반영합니다.
onRemove*(key: string) => void-사진 삭제를 요청합니다.
maxnumber5사진과 업로드 중 항목을 합친 최대 개수입니다.
maxSizenumber-사진당 최대 용량(MB)입니다. 생략하면 제한하지 않습니다.
onError(errors: FileError[]) => void-형식·용량·개수 제한 오류입니다.
placeholderstring'사진 추가'사진 추가 타일 문구입니다.
uploadingCountnumber0업로드 중인 사진 수와 live 상태입니다.
showPrimaryBadge / showCountbooleantrue대표 배지와 현재/최대 개수를 표시합니다.
accept / disabled / invalidstring / boolean / boolean-chooser와 상태 정책입니다.
invalidbooleanfalse오류 테두리를 표시하며 focus·open 상태에서도 유지합니다. aria-invalid도 함께 전달합니다.
variant'filled' | 'outline'filled공통 필드 표면 표현입니다. focus·hover는 중립 elevation을 사용합니다.
opaquebooleanfalseelevation 레이어 아래 불투명한 테마 바탕을 추가합니다. hover·focus·invalid 표현은 유지합니다.

Examples

자주 쓰는 조합을 살펴보고, 필요한 예시의 코드를 펼쳐 확인하세요.

대표 사진과 업로드 수명주기

첫 항목은 대표 배지로 표시되고, 업로드 중에는 추가 타일과 live 상태가 바뀝니다.

사진0/5

첫 번째 사진이 대표 사진으로 사용됩니다.

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: 사진 추가 타일과 삭제 버튼을 실행합니다.