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
55 changes: 27 additions & 28 deletions src/components/basicComponents/basicModal/BasicModal.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// BasicModal.tsx
import { createPortal } from 'react-dom'
import { AnimatePresence, motion } from 'framer-motion'
import { useCallback, useEffect, useRef } from 'react'
import { useNavigate, useLocation, Outlet } from 'react-router'
import { ChevronLeft } from 'lucide-react'
import { useEffect, useRef } from 'react'
import { useLocation, Outlet } from 'react-router'
import { storeModalOpen } from '@/store/storeModalOpen'
import ModalHeader from '@/components/modal/ModalHeader'
import { useModal } from '@/hooks/useModal'

/**
*
Expand All @@ -22,28 +23,33 @@ import { storeModalOpen } from '@/store/storeModalOpen'
* ```
*/
export default function BasicModal() {
const navigate = useNavigate()
const location = useLocation()
const modalRef = useRef<HTMLDivElement>(null)
const { closeModal } = useModal()

// Zustand store
const { isModalOpen, setModalOpen: SetModalOpen } = storeModalOpen()
const prevPath = location.state?.prevPath || '/'
const isModalOpen = storeModalOpen((state) => state.modalState.isModalOpen)
const setModalState = storeModalOpen((state) => state.setModalState)

// 닫기 로직
const handleClose = useCallback(() => {
SetModalOpen(false)
navigate(prevPath, { replace: true })
}, [SetModalOpen, navigate, prevPath])
// navigate를 통해 들어온 값들
const { title, subTitle } = storeModalOpen().modalState

// 무한 랜더링 방지 및 린트 회피용 ref
const setModalOpenRef = useRef(setModalState)
const isModalOpenRef = useRef(isModalOpen)
const closeModalRef = useRef(closeModal)

// location이 변경될 때마다 모달 상태 확인
useEffect(() => {
const smorc = setModalOpenRef.current
const imorc = isModalOpenRef.current

const isModalRoute = location.pathname.startsWith('/modal')
if (isModalRoute && !isModalOpen) {
SetModalOpen(true)
if (isModalRoute && !imorc) {
smorc({ isModalOpen: true })
document.body.style.overflow = 'hidden'
} else if (!isModalRoute && isModalOpen) {
SetModalOpen(false)
} else if (!isModalRoute && imorc) {
smorc({ isModalOpen: false })
document.body.style.overflow = 'unset'
}

Expand All @@ -55,19 +61,19 @@ export default function BasicModal() {
// 위의 else if는 경로 이동에 따른 모달 상태관리
// 아래의 return 클린업 함수는 비정상 종료 대응용 안전장치
return () => {
SetModalOpen(false)
smorc({ isModalOpen: false })
document.body.style.overflow = 'unset'
}
}, [location.pathname, isModalOpen, SetModalOpen])
}, [location.pathname])

// esc 누를시 이전 경로로 이동
useEffect(() => {
const handleEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') handleClose()
if (e.key === 'Escape') closeModalRef.current()
}
window.addEventListener('keydown', handleEsc)
return () => window.removeEventListener('keydown', handleEsc)
}, [handleClose])
}, [])

// 포커스 트랩
useEffect(() => {
Expand Down Expand Up @@ -105,7 +111,7 @@ export default function BasicModal() {
animate="visible"
exit="exit"
transition={{ duration: 0.25 }}
onClick={handleClose}
onClick={() => closeModalRef.current()}
>
<motion.div
className="relative max-h-[90vh] w-full max-w-md overflow-hidden rounded-xl bg-white shadow-2xl"
Expand All @@ -117,15 +123,8 @@ export default function BasicModal() {
transition={{ type: 'spring', stiffness: 280, damping: 25 }}
onClick={(e) => e.stopPropagation()}
>
<div className="sticky top-0 z-10 flex items-center border-b bg-white/90 p-4 backdrop-blur-sm">
<ChevronLeft
onClick={handleClose}
className="h-6 w-6 cursor-pointer text-gray-500 transition-colors hover:text-gray-700"
aria-label="창 닫기"
/>
</div>

<div className="max-h-[calc(90vh-3.5rem)] overflow-y-auto p-4">
<ModalHeader title={title} subTitle={subTitle} />
<Outlet />
</div>
</motion.div>
Expand Down
8 changes: 4 additions & 4 deletions src/components/modal/ModalHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { storeModalOpen } from '@/store/storeModalOpen'
import { useModal } from '@/hooks/useModal'
import { X } from 'lucide-react'

type ModalHeaderProps = {
Expand All @@ -7,11 +7,11 @@ type ModalHeaderProps = {
}

const ModalHeader = ({ title, subTitle = null }: ModalHeaderProps) => {
const { setModalOpen: setModalOpen } = storeModalOpen()
const { closeModal } = useModal()

const handleClickCancel = (e: React.MouseEvent) => {
e.preventDefault()
setModalOpen(false)
closeModal()
}

return (
Expand All @@ -20,7 +20,7 @@ const ModalHeader = ({ title, subTitle = null }: ModalHeaderProps) => {
<h1 className="text-lg font-semibold">{title}</h1>
<h2 className="text-sm font-normal text-gray-500">{subTitle}</h2>
</div>
<button onClick={(e) => handleClickCancel(e)}>
<button onClick={(e) => handleClickCancel(e)} className="cursor-pointer">
<X size={18} />
</button>
</header>
Expand Down
6 changes: 3 additions & 3 deletions src/components/modal/datePicker/DatePickerFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { storeModalOpen } from '@/store/storeModalOpen'
import { useModal } from '@/hooks/useModal'
import { BasicButton } from '../../basicComponents/BasicButton/BasicButton'

type DatePickerFooterProps = {
selected: Date | undefined
}

const DatePickerFooter = ({ selected }: DatePickerFooterProps) => {
const { setModalOpen } = storeModalOpen()
const { closeModal } = useModal()

const handleClickCancel = (e: React.MouseEvent) => {
e.preventDefault()
setModalOpen(false)
closeModal()
}
return (
<footer className="flex w-full items-center justify-between border-t border-gray-300 p-6">
Expand Down
7 changes: 3 additions & 4 deletions src/components/modal/reviewPosting/ReviewPostingModal.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { storeModalOpen } from '@/store/storeModalOpen'
import type { StudyGroup } from '../../../types/StudyGroup'
import { BasicButton } from '../../basicComponents/BasicButton/BasicButton'
import ReviewRating from './ReviewRating'
import ReviewText from './ReviewText'
import { useModal } from '@/hooks/useModal'
// import { useLoaderData } from 'react-router'

const ReviewPostingModal = () => {
Expand All @@ -28,12 +28,11 @@ const ReviewPostingModal = () => {
const endYear = end.getFullYear()
const endMonth = end.getMonth() + 1
const endDate = end.getDate()

const { setModalOpen: setModalOpen } = storeModalOpen()
const { closeModal } = useModal()

const handleClickCancel = (e: React.MouseEvent) => {
e.preventDefault()
setModalOpen(false)
closeModal()
}

return (
Expand Down
2 changes: 1 addition & 1 deletion src/components/navBar/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default function NavbarLayout() {
</div>
</nav>

<main className="flex w-full flex-1 items-center justify-center bg-gray-50">
<main className="mt-[65px] flex w-full flex-1 items-center justify-center bg-gray-50">
<Outlet />
</main>
</div>
Expand Down
55 changes: 51 additions & 4 deletions src/hooks/useModal.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { storeModalOpen } from '@/store/storeModalOpen'
import { useEffect, useRef } from 'react'
import { useNavigate, useLocation } from 'react-router'

/**
Expand All @@ -18,12 +20,57 @@ import { useNavigate, useLocation } from 'react-router'
export function useModal() {
const navigate = useNavigate()
const location = useLocation()
const isClosingRef = useRef(false)

const openModal = (modalPath: string) => {
navigate(modalPath, {
state: { prevPath: location.pathname },
// 모달 열기
const openModal = (modalPath: string, title: string, subTitle?: string) => {
const currentPath = location.pathname
navigate(modalPath)
storeModalOpen.getState().setModalState({
isModalOpen: true,
prevPath: currentPath,
title: title,
subTitle: subTitle,
})
}

return { openModal } as const
/**
* 모달에서 다른 모달로 이동(prevPath 옵션을 위해 분리)
* 모달 > 다른 모달 이동 완료된 상태에서 뒤로가기 : navigate(-1)
* 모달 > 다른 모달 이동 완료된 상태에서 모달 닫기 : closeModal()
*/
const modalToModal = (
modalPath: string,
title: string,
subTitle?: string
) => {
navigate(modalPath)
storeModalOpen.getState().setModalState({
isModalOpen: true,
title: title,
subTitle: subTitle,
})
}

// 모달 닫기
const closeModal = () => {
const { prevPath } = storeModalOpen.getState().modalState
if (prevPath) {
isClosingRef.current = true
navigate(prevPath, { replace: true })
}
}

// 모달 닫을때 상태 초기화
// 기존에 clearModal이 과도하게 실행되는 문제가 있었으나, isClosingRef 참조형으로 변경하여 해결
useEffect(() => {
const { modalState, clearModal } = storeModalOpen.getState()

if (isClosingRef.current && location.pathname === modalState.prevPath) {
clearModal()
isClosingRef.current = false
}
}, [location.pathname])

return { openModal, closeModal, modalToModal } as const
}
39 changes: 23 additions & 16 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,41 @@ import TestY from './test/TestY'
import BasicModal from './components/basicComponents/basicModal/BasicModal'
import ReviewPostingModal from './components/modal/reviewPosting/ReviewPostingModal'
import DatePickerModal from './components/modal/datePicker/DatePickerModal'
import TestModal from './test/TestModal'

const router = createBrowserRouter([
{
path: '/',
Component: App,
children: [],
},
{
path: '/modal',
Component: BasicModal,
children: [
{
path: '/modal/review_posting',
loader: () => {},
Component: ReviewPostingModal,
},
{
path: '/modal/review_detail',
path: '/testModal',
Component: TestModal,
},
{
path: '/modal/date_picker',
Component: DatePickerModal,
},
{
path: '/modal/choosing_lecture',
path: '/modal',
Component: BasicModal,
children: [
{
path: '/modal/review_posting',
loader: () => {},
Component: ReviewPostingModal,
},
{
path: '/modal/review_detail',
},
{
path: '/modal/date_picker',
Component: DatePickerModal,
},
{
path: '/modal/choosing_lecture',
},
],
},
],
},

{
path: '/testD',
Component: TestD,
Expand Down
26 changes: 22 additions & 4 deletions src/store/storeModalOpen.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
import { create } from 'zustand'

interface ModalOpen {
type ModalState = {
isModalOpen: boolean
setModalOpen: (isOpen: boolean) => void
prevPath: string
title: string
subTitle?: string
}
export const storeModalOpen = create<ModalOpen>((set) => ({
interface storeModalState {
modalState: ModalState

setModalState: (state: Partial<ModalState>) => void
clearModal: () => void
}

const initState: ModalState = {
isModalOpen: false,
setModalOpen: (isOpen) => set(() => ({ isModalOpen: isOpen })),
prevPath: '',
title: '',
subTitle: '',
}

export const storeModalOpen = create<storeModalState>((set) => ({
modalState: initState,
setModalState: (newState) =>
set((cur) => ({ modalState: { ...cur.modalState, ...newState } })),
clearModal: () => set(() => ({ modalState: initState })),
}))
23 changes: 22 additions & 1 deletion src/test/TestG.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
import NavbarLayout from '@/components/navBar/NavBar'
import { useModal } from '@/hooks/useModal'
import { Route, Routes } from 'react-router'

function TestG() {
return (
<Routes>
<Route path="/" element={<NavbarLayout />}></Route>
<Route path="/" element={<NavbarLayout />}>
<Route path="/" element={<ModalTestBtns />} />
</Route>
</Routes>
)
}

export default TestG

const routeArr = [
{ path: '/modal/review_posting', title: 'review_posting' },
{ path: '/modal/review_detail', title: 'review_detail' },
{ path: '/modal/date_picker', title: 'date_picker' },
{ path: '/modal/choosing_lecture', title: 'choosing_lecture' },
]

function ModalTestBtns() {
const { openModal } = useModal()
return (
<>
{routeArr.map((el) => (
<button key={el.path} onClick={() => openModal(el.path, el.title)} />
))}
</>
)
}
Loading