diff --git a/index.html b/index.html index d82565f..ee721cf 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,7 @@ + 자세 공작소 diff --git a/src/App.tsx b/src/App.tsx index e8821d0..5362fba 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,13 +2,16 @@ import { Router } from "@/routes" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import ModalsProvider from "./providers/ModalsProvider" const queryClient = new QueryClient() const App = (): React.ReactElement => { return ( - + + + ) } diff --git a/src/components/Crew/CrewList.tsx b/src/components/Crew/CrewList.tsx index 87765ad..c3cb789 100644 --- a/src/components/Crew/CrewList.tsx +++ b/src/components/Crew/CrewList.tsx @@ -1,14 +1,13 @@ import { group, groupsReq } from "@/api" import EmptyCrewImage from "@/assets/images/crew-empty.png" import CrewItem from "@/components/Crew/CrewItem" -import CreateCrewModal from "@/components/Modal/CreateCrewModal" -import JoinCrewModal from "@/components/Modal/JoinCrewModal" import { useGetGroups } from "@/hooks/useGroupMutation" import CreateCrewIcon from "@assets/icons/crew-create-button-icon.svg?react" import SortCrewIcon from "@assets/icons/crew-sort-icon.svg?react" import { ReactElement, useEffect, useRef, useState } from "react" - +import { useModals } from "@/hooks/useModals" import MyCrewRankingContainer from "./MyCrewRankingContainer" +import { modals } from "../Modal/Modals" const SORT_LIST = [ { sort: "userCount,desc", label: "크루원 많은 순" }, @@ -19,8 +18,6 @@ const CrewList = (): ReactElement => { const [sort, setSort] = useState(0) const [mode] = useState<"my" | "list">("my") const [isDropdownOpen, setIsDropdownOpen] = useState(false) - const [isJoinModalOpen, setIsJoinModalOpen] = useState(false) - const [isCreateModalOpen, setIsCreateModalOpen] = useState(false) const [params] = useState({ page: 0, @@ -30,11 +27,31 @@ const CrewList = (): ReactElement => { const { data, isLoading, isError } = useGetGroups(params) - const openJoinModal = (): void => setIsJoinModalOpen(true) - const closeJoinModal = (): void => setIsJoinModalOpen(false) + const { openModal } = useModals() + + const openCreateModal = (): void => { + openModal(modals.createCrewModal, { + onSubmit: () => { + console.log("open") + }, + }) + } + + const openJoinCrewModal = (): void => { + openModal(modals.joinCrewModal, { + onSubmit: () => { + console.log("open") + }, + }) + } - const openCreateModal = (): void => setIsCreateModalOpen(true) - const closeCreateModal = (): void => setIsCreateModalOpen(false) + const openInviteModal = (): void => { + openModal(modals.inviteCrewModal, { + onSubmit: () => { + console.log("open") + }, + }) + } const dropdownRef = useRef(null) @@ -72,7 +89,7 @@ const CrewList = (): ReactElement => { return (
{_groups.map((g) => ( - + ))}
) @@ -93,7 +110,7 @@ const CrewList = (): ReactElement => { return (
- {mode === "my" && } + {mode === "my" && } {/* header */}
전체크루(0)
@@ -129,8 +146,6 @@ const CrewList = (): ReactElement => { {/* list */} {isLoading ? "로딩 중입니다..." : isError ? "데이터를 불러오는데 실패했습니다." : createGroupList(data?.data)} - {}} /> - {}} />
) } diff --git a/src/components/Crew/MyCrewRankingContainer.tsx b/src/components/Crew/MyCrewRankingContainer.tsx index e60a728..7b37fdc 100644 --- a/src/components/Crew/MyCrewRankingContainer.tsx +++ b/src/components/Crew/MyCrewRankingContainer.tsx @@ -8,10 +8,11 @@ import RoutePath from "@/constants/routes.json" interface MyCrewRankingContainerProps { openCreateModal: () => void + openInviteModal: () => void } export default function MyCrewRankingContainer(props: MyCrewRankingContainerProps) { - const { openCreateModal } = props + const { openCreateModal, openInviteModal } = props return (
@@ -32,7 +33,10 @@ export default function MyCrewRankingContainer(props: MyCrewRankingContainerProp 7/30명
-
- + ) } diff --git a/src/components/Modal/InviteCrewModal.tsx b/src/components/Modal/InviteCrewModal.tsx index db91777..e3efb18 100644 --- a/src/components/Modal/InviteCrewModal.tsx +++ b/src/components/Modal/InviteCrewModal.tsx @@ -1,16 +1,11 @@ -import Modal from "@components/Modal" +import { ModalProps } from "@/contexts/ModalsContext" +import ModalContainer from "@components/ModalContainer" -interface CreateCrewModalProps { - isOpen: boolean - onClose: () => void - onSubmit: () => void -} - -const InviteCrewModal = (props: CreateCrewModalProps): React.ReactElement => { - const { isOpen, onClose, onSubmit } = props +const InviteCrewModal = (props: ModalProps): React.ReactElement => { + const { onClose, onSubmit } = props return ( - +
{/* header */}
@@ -32,7 +27,7 @@ const InviteCrewModal = (props: CreateCrewModalProps): React.ReactElement => { 초대 링크 복사하기
- + ) } diff --git a/src/components/Modal/JoinCrewModal.tsx b/src/components/Modal/JoinCrewModal.tsx index a363c9f..80b804c 100644 --- a/src/components/Modal/JoinCrewModal.tsx +++ b/src/components/Modal/JoinCrewModal.tsx @@ -1,17 +1,11 @@ -import Modal from "@components/Modal" +import ModalContainer from "@components/ModalContainer" import CrewJoinUserIcon from "@assets/icons/crew-join-user-icon.svg?react" import PrivateCrewIcon from "@assets/icons/crew-private-icon.svg?react" import { useState } from "react" +import { ModalProps } from "@/contexts/ModalsContext" -interface JoinCrewModalProps { - id: number - isOpen: boolean - onClose: () => void - onSubmit: () => void -} - -const JoinCrewModal = (props: JoinCrewModalProps): React.ReactElement => { - const { isOpen, onClose, onSubmit } = props +const JoinCrewModal = (props: ModalProps): React.ReactElement => { + const { onClose, onSubmit } = props const [joinCode, setJoinCode] = useState("") const [isCodeError, setIsCodeError] = useState(false) @@ -22,7 +16,7 @@ const JoinCrewModal = (props: JoinCrewModalProps): React.ReactElement => { } return ( - +
{/* header */}
@@ -94,7 +88,7 @@ const JoinCrewModal = (props: JoinCrewModalProps): React.ReactElement => { 크루 가입하기
- + ) } diff --git a/src/components/Modal/Modals.tsx b/src/components/Modal/Modals.tsx new file mode 100644 index 0000000..10f0cbe --- /dev/null +++ b/src/components/Modal/Modals.tsx @@ -0,0 +1,43 @@ +import { ModalsDispatchContext, ModalsStateContext } from "@/contexts/ModalsContext" +import { useContext } from "react" +import CreateCrewModal from "./CreateCrewModal" +import InviteCrewModal from "./InviteCrewModal" +import JoinCrewModal from "./JoinCrewModal" +import WithdrawCrewModal from "./WithdrawCrewModal" + +export const modals = { + createCrewModal: CreateCrewModal, + inviteCrewModal: InviteCrewModal, + joinCrewModal: JoinCrewModal, + withdrawCrewModal: WithdrawCrewModal, +} + +const Modals = (): React.ReactNode => { + const openedModals = useContext(ModalsStateContext) + const { close } = useContext(ModalsDispatchContext) + + return openedModals.map((modal, index) => { + const { Component, props } = modal + if (!props) return null + + const { onSubmit, onClose, ...rest } = props + + const handleClose = async (): Promise => { + if (typeof onClose === "function") { + await onClose() + } + close(Component) + } + + const handleSubmit = async (): Promise => { + if (typeof onSubmit === "function") { + await onSubmit() + } + handleClose() + } + + // eslint-disable-next-line max-len + return + }) +} +export default Modals diff --git a/src/components/Modal/WithdrawCrewModal.tsx b/src/components/Modal/WithdrawCrewModal.tsx index fb3253d..f5ebbf4 100644 --- a/src/components/Modal/WithdrawCrewModal.tsx +++ b/src/components/Modal/WithdrawCrewModal.tsx @@ -1,16 +1,11 @@ -import Modal from "@components/Modal" +import { ModalProps } from "@/contexts/ModalsContext" +import ModalContainer from "@components/ModalContainer" -interface CreateCrewModalProps { - isOpen: boolean - onClose: () => void - onSubmit: () => void -} - -const WithdrawCrewModal = (props: CreateCrewModalProps): React.ReactElement => { - const { isOpen, onClose, onSubmit } = props +const WithdrawCrewModal = (props: ModalProps): React.ReactElement => { + const { onClose, onSubmit } = props return ( - +
{/* header */}
@@ -37,7 +32,7 @@ const WithdrawCrewModal = (props: CreateCrewModalProps): React.ReactElement => {
-
+ ) } diff --git a/src/components/Modal.tsx b/src/components/ModalContainer.tsx similarity index 65% rename from src/components/Modal.tsx rename to src/components/ModalContainer.tsx index 75dc593..cd92083 100644 --- a/src/components/Modal.tsx +++ b/src/components/ModalContainer.tsx @@ -1,22 +1,22 @@ -import React from "react" +import React, { ReactNode } from "react" import ReactDOM from "react-dom" import CloseIcon from "@assets/icons/modal-close-icon.svg?react" -interface ModalProps { - isOpen: boolean - onClose: () => void - children: React.ReactNode +type ModalContainerProps = { + onClose?: () => void + children: ReactNode } -const Modal: React.FC = ({ isOpen, onClose, children }) => { - if (!isOpen) return null - +const ModalContainer: React.FC = ({ onClose, children }) => { + const handleClose = (): void => { + if (onClose && typeof onClose === "function") onClose() + } // Modal이 main 안에서 절대적으로 위치하도록 변경 return ReactDOM.createPortal(
{/* Close Button */} - {/* Modal Content */} @@ -27,4 +27,4 @@ const Modal: React.FC = ({ isOpen, onClose, children }) => { ) } -export default Modal +export default ModalContainer diff --git a/src/components/PoseDetector.tsx b/src/components/PoseDetector.tsx index 6389eea..cc34a2c 100644 --- a/src/components/PoseDetector.tsx +++ b/src/components/PoseDetector.tsx @@ -124,7 +124,8 @@ const PoseDetector: React.FC = () => { const req = { snapshot: { keypoints, score }, type: poseType } sendPoseMutation.mutate(req) cntRef.current = cntRef.current + 1 - if (isShowNoti) showNotification(`척추 건강 위험! ${getPoseName(poseType)} 감지! 자세를 바르게 앉아주세요.`) + if (isShowNoti) + showNotification(`척추 건강 위험! ${getPoseName(poseType)} 감지! 자세를 바르게 앉아주세요.`) } }, 30 * 1000) } diff --git a/src/components/index.ts b/src/components/index.ts index 9a9e1ad..23bd399 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,3 +1,3 @@ export { default as Camera } from "./Camera" export { default as PoseDetector } from "./PoseDetector" -export { default as Modal } from "./Modal" +export { default as ModalContainer } from "./ModalContainer" diff --git a/src/contexts/ModalsContext.ts b/src/contexts/ModalsContext.ts new file mode 100644 index 0000000..13e7df8 --- /dev/null +++ b/src/contexts/ModalsContext.ts @@ -0,0 +1,21 @@ +import { ComponentType, createContext } from "react" + +export type ModalProps = { + id?: number + onClose?: () => void + onSubmit?: () => void +} + +export type ModalComponent = ComponentType +export type ModalsState = Array<{ Component: ModalComponent; props?: ModalProps }> +export type ModalsDispatch = { + open: (Component: ModalComponent, props: ModalProps) => void + close: (Component: ModalComponent) => void +} + +export const ModalsStateContext = createContext([]) + +export const ModalsDispatchContext = createContext({ + open: () => {}, + close: () => {}, +}) diff --git a/src/hooks/useModals.ts b/src/hooks/useModals.ts new file mode 100644 index 0000000..4a894a8 --- /dev/null +++ b/src/hooks/useModals.ts @@ -0,0 +1,24 @@ +import { ModalComponent, ModalProps, ModalsDispatchContext } from "@/contexts/ModalsContext" +import { useContext } from "react" + +interface UseModalResult { + openModal: (Component: ModalComponent, props: ModalProps) => void + closeModal: (Component: ModalComponent) => void +} + +export const useModals = (): UseModalResult => { + const { open, close } = useContext(ModalsDispatchContext) + + const openModal = (Component: ModalComponent, props: ModalProps): void => { + open(Component, props) + } + + const closeModal = (Component: ModalComponent): void => { + close(Component) + } + + return { + openModal, + closeModal, + } +} diff --git a/src/hooks/usePushNotification.ts b/src/hooks/usePushNotification.ts index 3b034d8..07fdb7d 100644 --- a/src/hooks/usePushNotification.ts +++ b/src/hooks/usePushNotification.ts @@ -1,7 +1,14 @@ import { useState, useEffect } from "react" +interface UsePushNotificationResult { + hasPermission: boolean + isPermissionDenied: boolean + requestNotificationPermission: () => Promise + showNotification: (body: string) => void +} + // 커스텀 훅: 알림 권한을 확인하고 권한 변경을 감지 -const usePushNotification = () => { +const usePushNotification = (): UsePushNotificationResult => { const [hasPermission, setHasPermission] = useState(false) // 권한이 허용되었는지 여부 const [isPermissionDenied, setIsPermissionDenied] = useState(false) // 권한이 거부되었는지 여부 diff --git a/src/providers/ModalsProvider.tsx b/src/providers/ModalsProvider.tsx new file mode 100644 index 0000000..8968cbd --- /dev/null +++ b/src/providers/ModalsProvider.tsx @@ -0,0 +1,40 @@ +import Modals from "@/components/Modal/Modals" +import { + ModalsDispatchContext, + ModalsStateContext, + ModalsState, + ModalComponent, + ModalProps, +} from "@/contexts/ModalsContext" +import { PropsWithChildren, useMemo, useState } from "react" + +const ModalsProvider = ({ children }: PropsWithChildren): React.ReactNode => { + const [openedModals, setOpenedModals] = useState([]) + + const open = (Component: ModalComponent, props: ModalProps): void => { + console.log(Component, props) + setOpenedModals((modals) => { + return [...modals, { Component, props }] + }) + } + + const close = (Component: ModalComponent): void => { + setOpenedModals((modals) => { + return modals.filter((modal) => { + return modal.Component !== Component + }) + }) + } + + const dispatch = useMemo(() => ({ open, close }), []) + + return ( + + + {children} + + + + ) +} +export default ModalsProvider diff --git a/tsconfig.json b/tsconfig.json index 745944f..5e53a91 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,8 +28,10 @@ "@constants/*": ["src/constants/*"], "@pages/*": ["src/pages/*"], "@layouts/*": ["src/layouts/*"], - "@store": ["src/store/*"], - "@assets/*": ["src/assets/*"] + "@assets/*": ["src/assets/*"], + "@store/*": ["src/store/*"], + "@contexts/*": ["src/contexts/*"], + "@providers/*": ["src/providers/*"] } }, "include": ["src", "svg.d.ts"],