diff --git a/src/components/pages/create-group/fields/images-field/image-loading-bar/index.tsx b/src/components/pages/create-group/fields/images-field/image-loading-bar/index.tsx new file mode 100644 index 00000000..8df34e37 --- /dev/null +++ b/src/components/pages/create-group/fields/images-field/image-loading-bar/index.tsx @@ -0,0 +1,22 @@ +export const ImageLoadingBar = () => { + return ( +
+ +
+ ); +}; diff --git a/src/components/pages/create-group/fields/images-field/index.tsx b/src/components/pages/create-group/fields/images-field/index.tsx index 13f37e43..bebdbed5 100644 --- a/src/components/pages/create-group/fields/images-field/index.tsx +++ b/src/components/pages/create-group/fields/images-field/index.tsx @@ -2,14 +2,16 @@ import Image from 'next/image'; -import { useRef } from 'react'; +import { useRef, useState } from 'react'; import { AnyFieldApi } from '@tanstack/react-form'; import { Icon } from '@/components/icon'; +import { ImageLoadingBar } from '@/components/pages/create-group/fields/images-field/image-loading-bar'; +import { Hint } from '@/components/ui'; import { useUploadGroupImages } from '@/hooks/use-group/use-group-upload-images'; import { cn } from '@/lib/utils'; -import { ALLOWED_IMAGE_TYPES } from '@/types/service/common'; +import { validateImage } from '@/lib/validateImage'; import { PreUploadGroupImageResponse } from '@/types/service/group'; interface Props { @@ -17,24 +19,23 @@ interface Props { } export const GroupImagesField = ({ field }: Props) => { - const { mutateAsync } = useUploadGroupImages(); - - const onUploadImage = async (e: React.ChangeEvent) => { - const maxAllowed = 3 - field.state.value.length; - const files = e.target.files; + const [preUploadError, setPreUploadError] = useState(''); + const inputRef = useRef(null); - if (!files || files.length === 0) return; - if (files.length > maxAllowed) return; + const { mutateAsync, isPending } = useUploadGroupImages(); - const fileArray = Array.from(files); + const onUploadImage = async (e: React.ChangeEvent) => { + if (!e.target.files || !e.target.files.length) return; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const invalidFile = fileArray.find((file) => !ALLOWED_IMAGE_TYPES.includes(file.type as any)); + const fileArray = Array.from(e.target.files); - if (invalidFile) { - alert('jpg, png, webp 파일만 업로드 가능합니다.'); - e.target.value = ''; - return; + for (const file of fileArray) { + const { valid, error } = await validateImage(file); + if (!valid && error) { + setPreUploadError(error); + e.target.value = ''; + return; + } } const response = await mutateAsync({ @@ -42,6 +43,7 @@ export const GroupImagesField = ({ field }: Props) => { }); field.handleChange([...field.state.value, ...response.images]); + setPreUploadError(''); }; const onUploadImageButtonClick = () => { @@ -54,29 +56,32 @@ export const GroupImagesField = ({ field }: Props) => { field.handleChange(removedArray); }; - const inputRef = useRef(null); - return (
+ {preUploadError && }

최대 3개까지 업로드할 수 있어요.

diff --git a/src/components/ui/hint/index.tsx b/src/components/ui/hint/index.tsx index ff10b39c..cbe1777c 100644 --- a/src/components/ui/hint/index.tsx +++ b/src/components/ui/hint/index.tsx @@ -8,7 +8,10 @@ export const Hint = ({ className, message, ...props }: HintProps) => { return (

{message} diff --git a/src/lib/constants/image.ts b/src/lib/constants/image.ts index ac267a6b..c9bd3d02 100644 --- a/src/lib/constants/image.ts +++ b/src/lib/constants/image.ts @@ -1,5 +1,5 @@ export const IMAGE_CONFIG = { - maxSizeBytes: 20971520, // 20MB + maxSizeBytes: 10485760, // 10MB maxWidth: 2000, maxHeight: 2000, allowedTypes: ['image/jpeg', 'image/png', 'image/webp'], diff --git a/src/lib/validateImage.ts b/src/lib/validateImage.ts index e03ffa0a..454d9c80 100644 --- a/src/lib/validateImage.ts +++ b/src/lib/validateImage.ts @@ -1,11 +1,14 @@ import { IMAGE_CONFIG } from './constants/image'; export const validateImage = async (file: File): Promise<{ valid: boolean; error?: string }> => { - // 1. 확장자 검증 + // 1. 파일 확장자 & 파일 타입 검증 const fileName = file.name.toLowerCase(); const hasValidExtension = IMAGE_CONFIG.allowedExtensions.some((ext) => fileName.endsWith(ext)); + const hasValidType = IMAGE_CONFIG.allowedTypes.some((type) => file.type === type); - if (!hasValidExtension) { + const isValidImage = hasValidExtension && hasValidType; + + if (!isValidImage) { return { valid: false, error: `파일 확장자가 올바르지 않습니다. \n(${IMAGE_CONFIG.allowedExtensions.join(', ')}만 가능)`, @@ -17,7 +20,7 @@ export const validateImage = async (file: File): Promise<{ valid: boolean; error const currentSizeMB = (file.size / (1024 * 1024)).toFixed(0); return { valid: false, - error: `이미지 크기가 너무 큽니다. 최대 20MB까지 가능합니다. \n현재: ${currentSizeMB}MB`, + error: `이미지 크기가 너무 큽니다. 최대 10MB까지 가능합니다. \n현재: ${currentSizeMB}MB`, }; } diff --git a/src/types/service/common.ts b/src/types/service/common.ts index fc64f79b..221e7c85 100644 --- a/src/types/service/common.ts +++ b/src/types/service/common.ts @@ -16,5 +16,3 @@ export class CommonSuccessResponse { public data: T, ) {} } - -export const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp'] as const;