Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 75 additions & 30 deletions components/Modal/ImageUploadModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import React, { useEffect, useRef, useState } from 'react';

import Button from '../Button';
import Modal from './Modal';
import SnackBar from '../SnackBar';
import useSnackBar from '@/hooks/useSanckBar';

interface ImageUploadModalProps {
imageFile?: File | null;
Expand All @@ -11,89 +13,121 @@ interface ImageUploadModalProps {
onGetImageFile: (file: File | null) => void;
}

/**
* 이미지 업로드 모달 컴포넌트
* @param isOpen 모달 오픈 여부
* @param onClose 모달 닫기 함수
* @returns ImageUploadModal 컴포넌트
*/
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB 를 바이트로
const NOT_SVG_MESSAGE =
'JPG, PNG, GIF, WebP 형식의 이미지만 업로드 가능합니다.';
const SIZE_LIMIT_MESSAGE = '파일 크기는 5MB를 초과할 수 없습니다.';

const ImageUploadModal = ({
imageFile: initialImageFile = null,
isOpen,
onClose,
onGetImageFile,
}: ImageUploadModalProps) => {
// input 요소를 참조하기 위한 ref
const fileInputRef = useRef<HTMLInputElement>(null);
// 선택한 이미지의 미리보기 url
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [imageFile, setImageFile] = useState<File | null>(initialImageFile);
const [isDragging, setIsDragging] = useState(false); // 드래그 중인지 여부
const [isDragging, setIsDragging] = useState(false);
const [isValidFile, setIsValidFile] = useState<boolean>(true);
const { snackBarValues, snackBarOpen } = useSnackBar();

const validateFile = (file: File): boolean => {
setIsValidFile(true);

const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
snackBarOpen('fail', NOT_SVG_MESSAGE);
setIsValidFile(false);
return false;
}

if (file.size > MAX_FILE_SIZE) {
snackBarOpen('fail', SIZE_LIMIT_MESSAGE);
setIsValidFile(false);
return false;
}

return true;
};

// 파일 선택시
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// console.log('--- handleFileChange', e.target.files);
const file = e.target.files?.[0];
if (file) {
const previewUrl = URL.createObjectURL(file);
setPreviewUrl(previewUrl);
setImageFile(file);
if (validateFile(file)) {
const previewUrl = URL.createObjectURL(file);
setPreviewUrl(previewUrl);
setImageFile(file);
} else if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
};

// 이미지 파일 삭제
const handleImageDelete = () => {
// console.log('--- handleImageDelete');
if (fileInputRef.current) {
fileInputRef.current.value = '';
setPreviewUrl(null);
setImageFile(null);
setIsValidFile(true);
}
};

// 드래그 관련 이벤트
// 드래그 시작
const handleDragEnter = (e: React.DragEvent<HTMLElement>) => {
e.preventDefault();
e.stopPropagation(); // 이벤트 전파 중지
e.stopPropagation();
setIsDragging(true);

if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
const file = e.dataTransfer.items[0]?.getAsFile();
if (file) {
const allowedTypes = [
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
];
setIsValidFile(allowedTypes.includes(file.type));
}
}
};
// 드래그가 영역을 벗어날 때

const handleDragLeave = (e: React.DragEvent<HTMLElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
setIsValidFile(true); // 드래그가 끝나면 상태 초기화
};
// 드래그 중

const handleDragOver = (e: React.DragEvent<HTMLElement>) => {
e.preventDefault();
e.stopPropagation();
};
// 드래그로 파일을 드랍했을 때

const handleDrop = (e: React.DragEvent<HTMLElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);

// length로 파일 존재 여부를 먼저 체크
if (e.dataTransfer.files.length > 0) {
const file = e.dataTransfer.files[0];
if (file) {
if (file && validateFile(file)) {
setPreviewUrl(URL.createObjectURL(file));
setImageFile(file);
}
}
};

// 썸네일로 선택
const handleImageSelect = () => {
onGetImageFile(imageFile);
onClose();
};

useEffect(() => {
// cleanup
if (isOpen) {
setIsValidFile(true);
}
}, [isOpen]);

useEffect(() => {
return () => {
if (previewUrl !== null) {
URL.revokeObjectURL(previewUrl);
Expand All @@ -110,14 +144,14 @@ const ImageUploadModal = ({

return (
<Modal isOpen={isOpen} onClose={onClose}>
<div className="flex flex-col gap-5">
<div className="flex flex-col gap-5 bg-background">
<p className="text-center text-18sb">이미지</p>
<input
id="image"
type="file"
ref={fileInputRef}
onChange={handleFileChange}
accept="image/*"
accept="image/jpeg,image/png,image/gif,image/webp"
className="hidden"
/>
{previewUrl === null ? (
Expand All @@ -129,7 +163,9 @@ const ImageUploadModal = ({
onDrop={handleDrop}
className={`flex h-40 w-full cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed ${
isDragging
? 'border-green-400 bg-green-50'
? isValidFile
? 'border-green-300 bg-green-100'
: 'border-red-100 bg-red-50'
: 'border-gray-300 bg-gray-100 hover:border-gray-400'
}`}
>
Expand All @@ -142,6 +178,7 @@ const ImageUploadModal = ({
<p className="mt-2 text-14 text-gray-400">
클릭 또는 이미지를 드래그하여 올려주세요.
</p>
<p className="mt-1 text-12 text-gray-400">(최대 5MB)</p>
</label>
) : (
<div className="relative flex h-40 w-full items-center justify-center rounded-lg border-2 border-dashed">
Expand Down Expand Up @@ -173,6 +210,14 @@ const ImageUploadModal = ({
</Button>
</div>
</div>

<SnackBar
severity={snackBarValues.severity}
onClose={snackBarValues.onClose}
open={snackBarValues.open}
>
{snackBarValues.children}
</SnackBar>
</Modal>
);
};
Expand Down
4 changes: 2 additions & 2 deletions components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ const Modal = ({
/>

<div
className={`${width} relative max-h-[90vh] overflow-y-auto rounded-lg bg-white shadow-xl`}
className={`${width} relative max-h-[90vh] overflow-y-auto rounded-lg bg-background shadow-custom`}
>
{/* 닫기 버튼 영역 */}
<button
Expand All @@ -84,7 +84,7 @@ const Modal = ({
/>
</button>
{/* 컨텐츠 영역 */}
<div className="px-5 pb-5 pt-[60px]">{children}</div>
<div className="bg-background px-5 pb-5 pt-[60px]">{children}</div>
</div>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion components/Modal/WikiQuizModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ function WikiQuizModal({
}`}
>
<div
className={`flex size-[42px] items-center justify-center rounded-full bg-white ${
className={`flex size-[42px] items-center justify-center rounded-full bg-background ${
keyboardVisible ? '!size-auto' : ''
}`}
>
Expand Down
Loading