-
Notifications
You must be signed in to change notification settings - Fork 2
Feature/#22 접속 끊김 모달 컴포넌트 #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The head ref may contain hidden characters: "feature/#22_\uC811\uC18D-\uB04A\uAE40-\uBAA8\uB2EC-\uCEF4\uD3EC\uB10C\uD2B8"
Changes from all commits
1b674d1
159443b
cc862d2
de99f31
7e8f22f
78fb2ee
83e5a3e
aa2e57a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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> | ||
|
Comment on lines
+10
to
+27
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기도 #39 PR 리뷰 남겨드린것처럼 컨텐츠만 분리해서 작성하는게 좋을듯합니다! |
||
| ); | ||
| }; | ||
|
|
||
| export default DisconnectionModal; | ||
| 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); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 타입이 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> | ||
|
Comment on lines
+159
to
+178
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 svg 도 return 안쪽이 아닌 함수 바깥이나 별도 아이콘 파일로 public 폴더에 두는것이 좋아 보입니다. |
||
| </div> | ||
| ) : ( | ||
| '삽입하기' | ||
| )} | ||
| </button> | ||
| </div> | ||
| </div> | ||
| </Modal> | ||
| ); | ||
| }; | ||
|
|
||
| export default ImageUploadModal; | ||
| 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; |
| 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> | ||
|
Comment on lines
+10
to
+23
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기도 동일하게 컨텐츠에 대한 JSX를 리턴하게끔해주세용 |
||
| ); | ||
| }; | ||
|
|
||
| export default UnsavedChangesModal; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
여긴 공통컴포넌트로 수정해주세요!