diff --git a/components/ImageUploadModal.tsx b/components/ImageUploadModal.tsx new file mode 100644 index 0000000..a844867 --- /dev/null +++ b/components/ImageUploadModal.tsx @@ -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(null); + + // 선택한 이미지의 미리보기 url + const [previewUrl, setPreviewUrl] = useState(null); + + const [isDragging, setIsDragging] = useState(false); // 드래그 중인지 여부 + + const [isUpload, setIsUpload] = useState(false); + + // 카메라 아이콘 클릭시 input 요소 클릭이 되도록 + const handleCameraClick = () => { + fileInputRef.current?.click(); + }; + + // 파일 선택시 + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + // 파일을 url로 변환하여 미리보기 생성 + const imageUrl = URL.createObjectURL(file); + setPreviewUrl(imageUrl); + } + }; + + // 드래그 관련 이벤트 + // 드래그 시작 + const handleDragEnter = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); // 이벤트 전파 중지 + setIsDragging(true); + }; + // 드래그가 영역을 벗어날 때 + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + }; + // 드래그 중 + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }; + // 드래그로 파일을 드랍했을 때 + const handleDrop = (e: React.DragEvent) => { + 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 ( + +
+

이미지

+ + + {previewUrl ? ( +
+ 선택된 이미지 +
+ ) : ( +
+ 카메라 아이콘 +

+ 클릭 또는 이미지를 드래그하여 올려주세요 +

+
+ )} +
+ +
+
+
+ ); +}; + +export default ImageUploadModal; diff --git a/components/Modal.tsx b/components/Modal.tsx new file mode 100644 index 0000000..8df0d10 --- /dev/null +++ b/components/Modal.tsx @@ -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 ( +
+
+ {/* 닫기 버튼 영역 */} +
+ 닫기 아이콘 +
+ {/* 컨텐츠 영역 */} +
{children}
+
+
+ ); +}; + +export default Modal; diff --git a/components/WikiQuizModal.tsx b/components/WikiQuizModal.tsx new file mode 100644 index 0000000..b0ea980 --- /dev/null +++ b/components/WikiQuizModal.tsx @@ -0,0 +1,174 @@ +import Image from 'next/image'; +import { useEffect, useRef, useState } from 'react'; + +import Modal from './Modal'; + +interface WikiQuizModalProps { + isOpen: boolean; // 모달 오픈 여부 + onClose: () => void; // 모달 닫기 함수 + securityQuestion: string; // from user.profile.code + securityAnswer: string; + onQuizComplete: () => void; // 퀴즈 완료 함수 +} + +interface WarningTextProps { + warning: boolean; +} + +const WarningText = ({ warning }: WarningTextProps) => { + if (!warning) return null; + return ( +

정답이 아닙니다. 다시 입력해 주세요.

+ ); +}; + +/** + * 위키 퀴즈 모달 컴포넌트 + * @param isOpen 모달 오픈 여부 + * @param onClose 모달 닫기 함수 + * @param securityQuestion 질문 + * @param securityAnswer 답변 + * @param onQuizComplete 퀴즈 완료 함수 + * @returns WikiQuizModal 컴포넌트 + */ + +const WikiQuizModal = ({ + isOpen, + onClose, + securityQuestion, + securityAnswer, + onQuizComplete, +}: WikiQuizModalProps) => { + const [isCorrect, setIsCorrect] = useState(false); + const [userAnswer, setUserAnswer] = useState(''); + const [warning, setWarning] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + + const inputRef = useRef(null); + + // 정답 타이핑 관리 함수 + const handleUserAnswer = (e: React.ChangeEvent) => { + setUserAnswer(e.target.value); + setWarning(false); // 타이핑시 경고문 제거 + }; + + const handleQuizSubmit = async (e: React.FormEvent) => { + e.preventDefault(); // 새로고침 방지 + try { + if (!userAnswer) return; + + setIsSubmitting(true); // 제출 시 로딩 ui 표시 + // api 통신 로직 + await new Promise((resolve) => setTimeout(resolve, 1000)); // 실제 통신 대신 시뮬레이션션 + // 정답이면 + // { + // "registeredAt": "2024-12-16T07:56:44.829Z", + // "userId": 1799 + // } + // 오답이면 + // { + // "message": "보안 답변이 일치하지 않습니다." + // } + + // 답변 비교 전에 trim() + const cleanUserAnswer = userAnswer.trim(); + const cleanSecurityAnswer = securityAnswer?.trim() || ''; + if (cleanUserAnswer === cleanSecurityAnswer) { + setIsCorrect(true); + setWarning(false); + onQuizComplete(); + } else { + setWarning(true); + setUserAnswer(''); // 오답시 입력값 초기화 + inputRef.current?.focus(); // 오답시 input에 포커스 + } + } catch (error) { + console.error('Quiz 제출 error:', error); + alert('퀴즈 제출 중 오류가 발생했습니다.'); + } finally { + setIsSubmitting(false); // 제출 완료 시 로딩 종료 + } + }; + + useEffect(() => { + // 모달 오픈시 input 포커스 + if (isOpen) { + inputRef.current?.focus(); + } + setIsCorrect(false); // 모달 오픈시 정답 초기화 + setUserAnswer(''); // 모달 오픈시 입력값 초기화 + setWarning(false); // 모달 오픈시 경고문 초기화 + setIsCorrect(false); // 모달 오픈시 정답 초기화 + }, [isOpen]); + + return ( + +
+ 자물쇠 아이콘 +

다음 퀴즈를 맞추고

+

위키를 작성해 보세요.

+
+ {securityQuestion} +
+ + + + +
+

위키드는 지인들과 함께하는 즐거운 공간입니다.

+

지인에게 상처를 주지 않도록 작성해 주세요.

+
+
+ ); +}; + +export default WikiQuizModal; diff --git a/pages/test/modal.tsx b/pages/test/modal.tsx new file mode 100644 index 0000000..b0ff5d3 --- /dev/null +++ b/pages/test/modal.tsx @@ -0,0 +1,66 @@ +import { useState } from 'react'; + +// import DisconnectionModal from '@/components/DisconnectionModal'; +// import UnsavedChangesModal from '@/components/UnsavedChangesModal'; +import ImageUploadModal from '@/components/ImageUploadModal'; +import WikiQuizModal from '@/components/WikiQuizModal'; + +const QUESTION = '특별히 싫어하는 음식은?'; +const ANSWER = '카레'; + +function disconnect() { + // const [isDMOpen, setIsDMOpen] = useState(false); + // const [isUCOpen, setIsUCOpen] = useState(false); + const [isQuizOpen, setIsQuizOpen] = useState(false); + const [isImageOpen, setIsImageOpen] = useState(false); + // const onUCClose = () => setIsUCOpen(false); + // const onDMClose = () => setIsDMOpen(false); + const onQuizClose = () => setIsQuizOpen(false); + const onImageClose = () => setIsImageOpen(false); + + const handleQuizSuccess = () => { + alert('퀴즈를 성공하셨습니다.'); + setIsQuizOpen(false); + }; + + return ( +
+ {/* + */} + + + {/* + */} + + +
+ ); +} + +export default disconnect; diff --git a/public/icon/icon-camera.png b/public/icon/icon-camera.png new file mode 100644 index 0000000..b26cf43 Binary files /dev/null and b/public/icon/icon-camera.png differ diff --git a/public/icon/icon-close.svg b/public/icon/icon-close.svg new file mode 100644 index 0000000..b6181e2 --- /dev/null +++ b/public/icon/icon-close.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icon/icon-lock.png b/public/icon/icon-lock.png new file mode 100644 index 0000000..c318720 Binary files /dev/null and b/public/icon/icon-lock.png differ