From 985ab52b45c7023b36252441112b136b0f2c6080 Mon Sep 17 00:00:00 2001 From: isuk Date: Tue, 8 Aug 2023 01:44:25 +0900 Subject: [PATCH 1/5] =?UTF-8?q?FEAT=20:=20useModal.tsx=3D>useModals.tsx=20?= =?UTF-8?q?=20useModal=EC=97=90=20=EB=AA=A8=EB=8B=AC=20=EA=B0=9C=EC=88=98?= =?UTF-8?q?=20props=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Modal/useModal.tsx | 37 ----------------------- src/components/Modal/useModals.tsx | 47 ++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 37 deletions(-) delete mode 100644 src/components/Modal/useModal.tsx create mode 100644 src/components/Modal/useModals.tsx diff --git a/src/components/Modal/useModal.tsx b/src/components/Modal/useModal.tsx deleted file mode 100644 index 1cc44ac..0000000 --- a/src/components/Modal/useModal.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { Union } from "../../types/commonTypes"; - -const modalStates = ["open", "closed", "closing"] as const; -export type ModalState = Union; - -export default function useModal( - closingTime = 300, - afterClosed = () => void 0, -) { - const [state, setState] = useState("closed"); - - const openModal = useCallback(() => { - setState("open"); - }, []); - - const timeoutId = useRef(); - const closeModal = useCallback(() => { - setState("closing"); - timeoutId.current = window.setTimeout(() => { - setState("closed"); - afterClosed(); - }, closingTime); - }, [afterClosed, closingTime]); - - // clean up timeout on unmount - useEffect(() => () => window.clearTimeout(timeoutId.current), []); - - return useMemo( - () => ({ - state, - openModal, - closeModal, - }), - [closeModal, openModal, state], - ); -} diff --git a/src/components/Modal/useModals.tsx b/src/components/Modal/useModals.tsx new file mode 100644 index 0000000..6735b9b --- /dev/null +++ b/src/components/Modal/useModals.tsx @@ -0,0 +1,47 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Union } from "../../types/commonTypes"; + +const modalStates = ["open", "closed", "closing"] as const; +export type ModalState = Union; + +export default function useModals( + n: number, + closingTime = 300, + afterClosed = () => void 0, +) { + const [states, setStates] = useState( + [...Array(n)].fill("closed"), + ); + + const openModalById = useCallback( + (id: number) => () => { + setStates((prev) => prev.map((_, idx) => (id === idx ? "open" : _))); + }, + [], + ); + + const timeoutId = useRef(); + const closeModalById = useCallback( + (id: number) => () => { + setStates((prev) => prev.map((_, idx) => (id === idx ? "closing" : _))); + timeoutId.current = window.setTimeout(() => { + setStates((prev) => prev.map((_, idx) => (id === idx ? "closed" : _))); + afterClosed(); + }, closingTime); + }, + [afterClosed, closingTime], + ); + + // clean up timeout on unmount + useEffect(() => () => window.clearTimeout(timeoutId.current), []); + + return useMemo( + () => + states.map((state, id) => { + const openModal = openModalById(id); + const closeModal = closeModalById(id); + return { state, openModal, closeModal }; + }), + [openModalById, closeModalById, states], + ); +} From 7d50dd9dbe74c484cfede93c92ab6596a99455eb Mon Sep 17 00:00:00 2001 From: isuk Date: Tue, 8 Aug 2023 01:45:58 +0900 Subject: [PATCH 2/5] =?UTF-8?q?UPDATE=20:=20=EC=9C=84=20=EC=BB=A4=EB=B0=8B?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EB=B3=80=EA=B2=BD=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EB=8C=80=EC=9D=91;=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EB=AA=A8=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solve/ProblemDescription/ProblemDescription.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/solve/ProblemDescription/ProblemDescription.tsx b/src/components/solve/ProblemDescription/ProblemDescription.tsx index 8b920b2..30789d5 100644 --- a/src/components/solve/ProblemDescription/ProblemDescription.tsx +++ b/src/components/solve/ProblemDescription/ProblemDescription.tsx @@ -1,5 +1,5 @@ import styled from "styled-components"; -import useModal from "../../Modal/useModal"; +import useModals from "../../Modal/useModals.tsx"; import Modal from "../../Modal/Modal"; import { BoldText, HorizontalLine, TestCase, Text } from "./common"; import TestCaseModal from "./TestCaseModal"; @@ -39,7 +39,7 @@ export default function ProblemDescription({ [setCustomTestCases], ); - const modalHandle = useModal(); + const [modalHandle] = useModals(1); return ( // TODO: 데이터 api 연결 From aafa703d4e8b1dad32e5baa204aecc3b9be255a5 Mon Sep 17 00:00:00 2001 From: isuk Date: Tue, 8 Aug 2023 01:57:28 +0900 Subject: [PATCH 3/5] =?UTF-8?q?UPDATE=20:=20import=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Modal/Modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 9c8f6fa..a7d7ec5 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -1,5 +1,5 @@ import styled from "styled-components"; -import { ModalState } from "./useModal"; +import { ModalState } from "./useModals"; import { ReactNode } from "react"; import { zIndex } from "../../lib/zIndex"; From 122a936a205f59f86d36af1690480d11448f2641 Mon Sep 17 00:00:00 2001 From: isuk Date: Tue, 8 Aug 2023 01:58:19 +0900 Subject: [PATCH 4/5] =?UTF-8?q?FEAT=20:=20pinned=20announcements=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=A4=EB=8A=94=20api=20request=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/announcement.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/apis/announcement.ts b/src/apis/announcement.ts index f699af5..83a7eb1 100644 --- a/src/apis/announcement.ts +++ b/src/apis/announcement.ts @@ -5,3 +5,10 @@ export const getAllAnnouncements = () => getRequest<{ items: TAnnouncement[] }>("/announcements/", {}, false).then( (res) => res.items.reverse(), ); + +export const getPinnedAnnouncements = () => + getRequest<{ items: TAnnouncement[] }>( + "/announcements/pinned", + {}, + false, + ).then((res) => res.items); From 4b54de8c8e43ef96b125484d9f22ae751fa79a80 Mon Sep 17 00:00:00 2001 From: isuk Date: Tue, 8 Aug 2023 01:59:34 +0900 Subject: [PATCH 5/5] =?UTF-8?q?FEAT=20:=20=EA=B3=B5=EC=A7=80=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EC=97=AC=EB=9F=AC=20=EA=B0=9C=20=EB=9D=84=EC=9A=B0?= =?UTF-8?q?=EA=B8=B0=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/home/NotificationModal.tsx | 25 +++-- src/pages/Home.tsx | 108 ++++++++++++---------- 2 files changed, 70 insertions(+), 63 deletions(-) diff --git a/src/components/home/NotificationModal.tsx b/src/components/home/NotificationModal.tsx index 9c6d4fa..2e90f15 100644 --- a/src/components/home/NotificationModal.tsx +++ b/src/components/home/NotificationModal.tsx @@ -1,31 +1,28 @@ import styled from "styled-components"; import { Link } from "react-router-dom"; -import { useQuery } from "@tanstack/react-query"; -import { getAllAnnouncements } from "../../apis/announcement"; +import { TAnnouncement } from "../../types/apiTypes"; type NotificationModalProps = { + announcement: TAnnouncement; + index: number; closeModal: () => void; setDontShowModalDate: (date: number) => void; }; export default function NotificationModal({ + announcement, + index, closeModal, setDontShowModalDate, }: NotificationModalProps) { - const { data: latestAnnouncement } = useQuery({ - queryKey: ["announcement"], - queryFn: () => getAllAnnouncements().then((res) => res[0]), - staleTime: 1, - }); return ( -
+
- {latestAnnouncement?.title} - {latestAnnouncement?.content} - {/* TODO : 오류 해결 */} + {announcement.title} + {announcement.content} 자세히보기 @@ -44,10 +41,10 @@ export default function NotificationModal({ ); } -const Article = styled.article` +const Article = styled.article<{ $index: number }>` position: fixed; - top: 124px; - left: 164px; + top: ${({ $index }) => `${124 + $index * 50}px`}; + left: ${({ $index }) => `${164 + $index * 40}px`}; width: 405px; border-radius: 15px; background-color: #fff; diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 8cebe43..b88f8a3 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -5,72 +5,82 @@ import Introduce from "../components/home/Introduce"; import Member from "../components/home/Member"; import Service from "../components/home/Service"; import ToHomePage from "../components/home/ToHomePage"; -import useModal from "../components/Modal/useModal"; +import useModals from "../components/Modal/useModals"; import Modal from "../components/Modal/Modal"; import NotificationModal from "../components/home/NotificationModal"; import { useEffect } from "react"; import Header from "../components/home/Header/Header"; -import { getAllAnnouncements } from "../apis/announcement"; +import { getPinnedAnnouncements } from "../apis/announcement"; import { useQuery } from "@tanstack/react-query"; const LOCAL_STORAGE_KEY_DONT_SHOW_MODAL_DATE = "dontShowExpireDate"; -const LOCAL_STORAGE_KEY_LATEST_ANNOUNCEMENT_ID = "latestAnnouncementId"; +const localStorageKeyOfAnnouncementId = (announcementId: number) => + `${LOCAL_STORAGE_KEY_DONT_SHOW_MODAL_DATE}_${announcementId}`; export default function Home() { - const modalHandle = useModal(0); - const { data: latestAnnouncement } = useQuery({ - queryKey: ["announcement", "latest"], - queryFn: () => getAllAnnouncements().then((res) => res[0]), - refetchInterval: 1000 * 60, - staleTime: Infinity, - retry: 0, + const { data: pinnedAnnouncements } = useQuery({ + queryKey: ["announcement", "pinned"], + queryFn: () => getPinnedAnnouncements().then(), + staleTime: 1, + initialData: [], }); + const modalHandles = useModals(5, 0); // 최대 5개 띄운다 + // 공지와 모달핸들러를 하나씩 짝짓는다 + const announcementModalHandlePairs = [ + ...Array(pinnedAnnouncements?.length), + ].map((_, idx) => ({ + announcement: pinnedAnnouncements[idx], + modalHandle: modalHandles[idx], + })); useEffect(() => { - const LATEST_ANNOUNCEMENT_ID = Number( - localStorage.getItem(LOCAL_STORAGE_KEY_LATEST_ANNOUNCEMENT_ID), - ); - if ( - latestAnnouncement && - latestAnnouncement.id !== LATEST_ANNOUNCEMENT_ID - ) { - localStorage.setItem(LOCAL_STORAGE_KEY_DONT_SHOW_MODAL_DATE, ""); - localStorage.setItem( - LOCAL_STORAGE_KEY_LATEST_ANNOUNCEMENT_ID, - latestAnnouncement.id.toString(), - ); - } - - const DONT_SHOW_MODAL_DATE = Number( - localStorage.getItem(LOCAL_STORAGE_KEY_DONT_SHOW_MODAL_DATE), - ); - if (!DONT_SHOW_MODAL_DATE) { - modalHandle.openModal(); - } else if (new Date().getDate() !== DONT_SHOW_MODAL_DATE) { - modalHandle.openModal(); - localStorage.setItem(LOCAL_STORAGE_KEY_DONT_SHOW_MODAL_DATE, ""); - } - }, [latestAnnouncement]); + announcementModalHandlePairs.forEach(({ announcement, modalHandle }) => { + // '오늘 그만보기'의 처리를 각 단위마다 실행 + const KEY = localStorageKeyOfAnnouncementId(announcement.id); + const DONT_SHOW_MODAL_DATE = Number(localStorage.getItem(KEY)); + if (!DONT_SHOW_MODAL_DATE) { + modalHandle.openModal(); + } else if (new Date().getDate() !== DONT_SHOW_MODAL_DATE) { + modalHandle.openModal(); + localStorage.setItem(KEY, ""); + } + }); + // api 응답이 성공했을 때 처리하기 위하여 dep array에 pinnedAnnouncements를 추가 + }, [pinnedAnnouncements]); return ( <>
- void 0} - modalContainerBackgroundColor="rgba(0, 0, 0, 0.15)" - > - { - localStorage.setItem( - LOCAL_STORAGE_KEY_DONT_SHOW_MODAL_DATE, - date.toString(), - ); - }} - /> - + {announcementModalHandlePairs + .filter(({ announcement }) => { + const KEY = localStorageKeyOfAnnouncementId(announcement.id); + const DONT_SHOW_MODAL_DATE = Number(localStorage.getItem(KEY)); + return !DONT_SHOW_MODAL_DATE; + }) + .map(({ announcement, modalHandle }, idx) => { + return ( + void 0} + // 가장 아래에 위치할 모달을 제외하고는 transparent로 처리 + modalContainerBackgroundColor={ + idx === 0 ? "rgba(0, 0, 0, 0.15)" : "transparent" + } + > + { + const KEY = `${LOCAL_STORAGE_KEY_DONT_SHOW_MODAL_DATE}_${announcement.id}`; + localStorage.setItem(KEY, date.toString()); + }} + /> + + ); + })}