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
31 changes: 31 additions & 0 deletions components/DisconnectionModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Modal from './Modal';

interface DisconnectionModalProps {
isOpen: boolean;
onClose: () => void;
}

const DisconnectionModal = ({ isOpen, onClose }: DisconnectionModalProps) => {
return (
<Modal isOpen={isOpen} onClose={onClose}>
<div className="flex flex-col gap-2">
<p className="text-lg font-bold">
5분 이상 글을 쓰지 않아 접속이 끊어졌어요.
</p>
<p className="text-sm">
위키 참여하기를 통해 다시 위키를 수정해 주세요.
</p>
<div className="flex justify-end">
<button
onClick={onClose}
className="mt-2 w-20 rounded-custom bg-green-200 px-6 py-2 text-background"
>
확인
</button>
</div>
</div>
</Modal>
);
};

export default DisconnectionModal;
190 changes: 190 additions & 0 deletions components/ImageUploadModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import Image from 'next/image';
import { useEffect, useRef, useState } from 'react';

import Modal from './Modal';

interface ImageUploadModalProps {
isOpen: boolean;
onClose: () => void;
}

/**
* 이미지 업로드 모달 컴포넌트
* @param isOpen 모달 오픈 여부
* @param onClose 모달 닫기 함수
* @returns ImageUploadModal 컴포넌트
*/

const ImageUploadModal = ({ isOpen, onClose }: ImageUploadModalProps) => {
// input 요소를 참조하기 위한 ref
const fileInputRef = useRef<HTMLInputElement>(null);

// 선택한 이미지의 미리보기 url
const [previewUrl, setPreviewUrl] = useState<string | null>(null);

const [isDragging, setIsDragging] = useState(false); // 드래그 중인지 여부

const [isUpload, setIsUpload] = useState(false);

// 카메라 아이콘 클릭시 input 요소 클릭이 되도록
const handleCameraClick = () => {
fileInputRef.current?.click();
};

// 파일 선택시
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
// 파일을 url로 변환하여 미리보기 생성
const imageUrl = URL.createObjectURL(file);
setPreviewUrl(imageUrl);
}
};

// 드래그 관련 이벤트
// 드래그 시작
const handleDragEnter = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation(); // 이벤트 전파 중지
setIsDragging(true);
};
// 드래그가 영역을 벗어날 때
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
};
// 드래그 중
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
};
// 드래그로 파일을 드랍했을 때
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);

const file = e.dataTransfer.files?.[0];
if (file) {
const imageUrl = URL.createObjectURL(file);
setPreviewUrl(imageUrl);
}
};

const handleImageUpload = async () => {
try {
setIsUpload(true);
// 이미지 업로그 api 통신 로직
await new Promise((resolve) => setTimeout(resolve, 1000));
setIsUpload(false);
alert('이미지 업로드 성공');
onClose();
} catch (error) {
console.error('이미지 업로드 error:', error);
alert('이미지 업로드 중 오류가 발생했습니다.');
} finally {
setIsUpload(false);
}
};

useEffect(() => {
return () => {
// 이전 미리보기 url 정리
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
setPreviewUrl(null);
}
};
}, [previewUrl, isOpen]);

return (
<Modal isOpen={isOpen} onClose={onClose}>
<div className="flex flex-col gap-4">
<p className="text-lg font-bold">이미지</p>
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
accept="image/*"
className="hidden"
/>

{previewUrl ? (
<div className="relative h-40 w-full overflow-hidden rounded-lg">
<Image
src={previewUrl}
alt="선택된 이미지"
layout="fill" // 부모 요소 크기에 맞게
objectFit="contain" // 비율 유지
/>
</div>
) : (
<div
onClick={handleCameraClick}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
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'
: 'border-gray-300 bg-gray-100 hover:border-gray-400'
}`}
>
<Image
src="/icon/icon-camera.png"
alt="카메라 아이콘"
width={40}
height={40}
/>
<p className="text-sm text-gray-500">
클릭 또는 이미지를 드래그하여 올려주세요
</p>
</div>
)}
<div className="flex justify-end">
<button
className={`w-40 rounded-custom px-6 py-2 text-background ${
previewUrl
? 'cursor-pointer bg-green-200'
: 'cursor-not-allowed bg-gray-300'
}`}
disabled={!previewUrl || isUpload}
onClick={handleImageUpload}
>
{isUpload ? (
<div className="flex items-center justify-center">
<span className="mr-2">확인 중</span>
<svg
className="h-5 w-5 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
</div>
) : (
'삽입하기'
)}
</button>
</div>
</div>
</Modal>
);
};

export default ImageUploadModal;
74 changes: 74 additions & 0 deletions components/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import Image from 'next/image';
import { useEffect } from 'react';

interface ModalProps {
isOpen: boolean; // 모달 오픈 여부
onClose: () => void; // 모달 닫기 함수
children: React.ReactNode; // 모달 내용
width?: string; // 모달 너비
}

/**
* 모달 공통 레이아웃 컴포넌트
* @param isOpen 모달 오픈 여부
* @param onClose 모달 닫기 함수
* @param children 모달 내용
* @param width 모달 너비
* @returns Modal 컴포넌트
*/

const Modal = ({
isOpen,
onClose,
children,
width = 'w-11/12 ta:w-3/4 pc:w-1/2',
}: ModalProps) => {
// 모달이 닫혀있으면 null 반환
if (!isOpen) return null;

// 배경 클릭시 모달 닫기
const handleBackGroundClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
onClose();
}
};

// esc 키로 모달 닫기
useEffect(() => {
const handleEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
};
window.addEventListener('keydown', handleEsc);
return () => window.removeEventListener('keydown', handleEsc);
}, []);

return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
onClick={handleBackGroundClick}
>
<div
className={`${width} relative max-h-[90vh] overflow-y-auto rounded-lg bg-white shadow-xl`}
>
{/* 닫기 버튼 영역 */}
<div
className="absolute right-0 top-0 m-2 cursor-pointer"
onClick={onClose}
>
<Image
src="/icon/icon-close.svg"
alt="닫기 아이콘"
width={22}
height={22}
/>
</div>
{/* 컨텐츠 영역 */}
<div className="p-6">{children}</div>
</div>
</div>
);
};

export default Modal;
27 changes: 27 additions & 0 deletions components/UnsavedChangesModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Modal from './Modal';

interface UnsavedChangesModalProps {
isOpen: boolean;
onClose: () => void;
}

const UnsavedChangesModal = ({ isOpen, onClose }: UnsavedChangesModalProps) => {
return (
<Modal isOpen={isOpen} onClose={onClose}>
<div className="flex flex-col gap-2">
<p className="text-lg font-bold">저장하지 않고 나가시겠어요?</p>
<p className="text-sm">작성하신 모든 내용이 사라집니다.</p>
<div className="flex justify-end">
<button
onClick={onClose}
className="mt-2 w-40 rounded-custom bg-red-100 px-6 py-2 text-background"
>
페이지 나가기
</button>
</div>
</div>
</Modal>
);
};

export default UnsavedChangesModal;
Loading
Loading