달력 (히트맵) 만들기 (1) 이어서…
이전 글에서는 달력을 만들었다면 이번 글에서는 날짜에 해당하는 포스트 수와 Home에서 이번 달 만이 아닌 다른 달과 년도를 선택할 수 있는 모달 창과 월 별 총 포스팅 수, 달력을 만든<PostHeatMap /> 에 넘겨줄 props 를 구한다.
날짜에 해당하는 포스트 수
달력을 만들기 전에 날짜마다 포스팅 된 글 수를 구한다.
createPostCount[x] = (createPostCount[x] || 0) + 1 설명
// postHeatMap.tsx const createPost = blogs.results.map((x: { created_time: string }) => { const create = new Date(x.created_time); const korDate = new Date( create.getTime() - create.getTimezoneOffset() * 60000 ) .toISOString() .split("T")[0]; return korDate; }); const createPostCount: PostCountType = {}; createPost.forEach((x: string | number) => { createPostCount[x] = (createPostCount[x] || 0) + 1; });
년도, 월 선택 모달 창
먼저 표기할 month , year를 배열에 담는다.
month는 영어로도 표기하고 싶어 배열 안 객체 [ { monthEng: "Jan", monthNum: “01”}, … ]로 구성했다.
const engMonthName = [ { monthEng: DEFINE.MONTHS.JAN.ENG, monthNum: DEFINE.MONTHS.JAN.NUM }, ... { monthEng: DEFINE.MONTHS.DEC.ENG, monthNum: DEFINE.MONTHS.DEC.NUM }, ]; const years = [2023, 2024];
useState를 활용해 monthList에 month 영문과 숫자를 저장하고 yearList에는 년도, showMonthModal는 boolean으로 모달 창 show 여부를 저장한다.
selectMonth() , toggleMonthList() 함수를 만들어 클릭 이벤트 발생 시 값이 변경 되게 했다.
⭐ 포스팅을 9월 말 부터 시작하여 2023.09 부터 선택 가능하게 하기 위해 2023년 선택 시 9월이전 달은 미 표기 했다.
const today = new Date(); const year = today.getFullYear(); const engMonth = engMonthName[today.getMonth()].monthEng; const numMonth = engMonthName[today.getMonth()].monthNum; const [monthList, setMonthList] = useState({ engMonth, numMonth }); const [yearList, setYearList] = useState(year); // modal const [showMonthModal, setShowMonthModal] = useState(false); const selectMonth = (engMonth: string, numMonth: string) => { setMonthList({ engMonth, numMonth }); }; const toggleMonthList = () => { setShowMonthModal((prev) => !prev); }; // <div className="text-base cursor-pointer relative"> <span onClick={toggleMonthList} ref={dropMonthMenuBtnRef} > {yearList}. {monthList.engMonth} </span> {showMonthModal && ( <div ref={dropMonthMenuRef}> <ul> {years.map((year) => ( <li key={year} onClick={() => { if (matchExceptMonth(monthList.engMonth)) { if (year !== 2023) { setYearList(year); } } else { setYearList(year); } }} className={cls( yearList === year ? "month-selected" : "", matchExceptMonth(monthList.engMonth) && year === 2023 ? "month-disabled" : "month-noneDisabled", "month-base" )} > {year} </li> ))} </ul> <ul className="h-48 overflow-y-auto scrollbar-none"> {yearList === 2023 ? engMonthName.slice(8).map((mon) => ( <li key={mon.monthEng} onClick={() => { if ( !( yearList === 2023 && matchExceptMonth(mon.monthEng) ) ) { selectMonth(mon.monthEng, mon.monthNum); } }} className={cls( monthList.engMonth === mon.monthEng ? "month-selected" : "month-noneDisabled", "month-base" )} > {mon.monthEng} </li> )) : engMonthName.map((mon) => ( <li key={mon.monthEng} onClick={() => { if ( !( yearList === 2023 && matchExceptMonth(mon.monthEng) ) ) { selectMonth(mon.monthEng, mon.monthNum); } }} className={cls( monthList.engMonth === mon.monthEng ? "month-selected" : "month-noneDisabled", "month-base" )} > {mon.monthEng} </li> ))} </ul> </div> )} </div>
모달 오픈 시 body scroll 방지
useEffect를 활용해 showMonthModal변경을 감지해 모달 오픈 시 body scroll 방지했다.
관련 글 : https://min-sun.vercel.app/blog/fa29c907-9d40-46ef-8fee-1b40bd112fab
const preventScroll = () => { const currentScrollY = window.scrollY; document.body.style.position = "fixed"; document.body.style.width = "100%"; document.body.style.top = `-${currentScrollY}px`; document.body.style.overflowY = "scroll"; return currentScrollY; }; const allowScroll = (prevScrollY: number) => { document.body.style.position = ""; document.body.style.width = ""; document.body.style.top = ""; document.body.style.overflowY = ""; window.scrollTo(0, prevScrollY); }; useEffect(() => { if (showMonthModal) { const prevScrollY = preventScroll(); return () => { allowScroll(prevScrollY); }; } }, [showMonthModal]);
모달 외부 클릭 시 닫기
pageState 처럼 모달 외부 클릭 시 닫힐 수 있게 적용했다.
관련 글 : https://min-sun.vercel.app/blog/f837f57d-3a2e-47f6-b1e3-be23cc132876
const dropMonthMenuBtnRef = useRef<HTMLDivElement | null>(null); const dropMonthMenuRef = useRef<HTMLDivElement | null>(null); useEffect(() => { const handleClickOutsideClose = (e: MouseEvent) => { if ( showMonthModal && !dropMonthMenuRef.current?.contains(e.target as Node) && !dropMonthMenuBtnRef.current?.contains(e.target as Node) ) setShowMonthModal(false); }; document.addEventListener("click", handleClickOutsideClose); return () => document.removeEventListener("click", handleClickOutsideClose); }, [showMonthModal]); const matchExceptMonth = (select: string) => { return exceptMonth.some((exceptMonth) => exceptMonth.monthEng === select); }; // <div className="text-base cursor-pointer relative"> <span onClick={toggleMonthList} ref={dropMonthMenuBtnRef} > {yearList}. {monthList.engMonth} </span> {showMonthModal && ( <div ref={dropMonthMenuRef} > // .... 선택하는 년도, 월 </div> )} </div>
월 별 포스팅 수
createPost()
mathMonth()
const createPost = blogs.results.map((x: { created_time: string }) => { const create = new Date(x.created_time); const korDate = new Date( create.getTime() - create.getTimezoneOffset() * 60000 ) .toISOString() .split("T")[0]; return korDate; }); const mathMonth = createPost.filter( (x: string) => x.slice(0, 7) === yearList + "-" + monthList.numMonth ); // 월별 포스트 수 <span> {mathMonth.length > 0 ? mathMonth.length : 0} posts in{" "} {monthList.engMonth} </span>
<PostHeatMap />
PostHeatMap 컴포넌트에 블로그 데이터와 변경되는 년도, 월 값을 넘겨 준다.
<div className="mt-2 h-full w-full"> <PostHeatMap blogs={blogs} year={yearList} month={monthList.numMonth} /> </div>
전체코드
import { DATABASE_ID_BLOG, TOKEN } from "libs/config"; import { useEffect, useRef, useState } from "react"; import dynamic from "next/dynamic"; import DEFINE from "@/constant/Global"; import { BlogistObject, ListResults } from "@/InterfaceGather"; const PostHeatMap = dynamic( () => import("@/components/ScreenElement/postHeatMap"), { ssr: false, } ); export default function Home({ blogs }: BlogistObject) { const today = new Date(); const year = today.getFullYear(); const engMonthName = [ { monthEng: DEFINE.MONTHS.JAN.ENG, monthNum: DEFINE.MONTHS.JAN.NUM }, ... { monthEng: DEFINE.MONTHS.DEC.ENG, monthNum: DEFINE.MONTHS.DEC.NUM }, ]; const years = [2023, 2024]; const engMonth = engMonthName[today.getMonth()].monthEng; const numMonth = engMonthName[today.getMonth()].monthNum; const createPost = blogs.results.map((x: { created_time: string }) => { const create = new Date(x.created_time); const korDate = new Date( create.getTime() - create.getTimezoneOffset() * 60000 ) .toISOString() .split("T")[0]; return korDate; }); const [monthList, setMonthList] = useState({ engMonth, numMonth }); const [yearList, setYearList] = useState(year); const [showMonthModal, setShowMonthModal] = useState(false); const selectMonth = (engMonth: string, numMonth: string) => { setMonthList({ engMonth, numMonth }); }; const toggleMonthList = () => { setShowMonthModal((prev) => !prev); }; const mathMonth = createPost.filter( (x: string) => x.slice(0, 7) === yearList + "-" + monthList.numMonth ); const exceptMonth = engMonthName.slice(0, 8).map((mon) => mon); const preventScroll = () => { const currentScrollY = window.scrollY; document.body.style.position = "fixed"; document.body.style.width = "100%"; document.body.style.top = `-${currentScrollY}px`; document.body.style.overflowY = "scroll"; return currentScrollY; }; const allowScroll = (prevScrollY: number) => { document.body.style.position = ""; document.body.style.width = ""; document.body.style.top = ""; document.body.style.overflowY = ""; window.scrollTo(0, prevScrollY); }; useEffect(() => { if (showMonthModal) { const prevScrollY = preventScroll(); return () => { allowScroll(prevScrollY); }; } }, [showMonthModal]); const dropMonthMenuBtnRef = useRef<HTMLDivElement | null>(null); const dropMonthMenuRef = useRef<HTMLDivElement | null>(null); useEffect(() => { const handleClickOutsideClose = (e: MouseEvent) => { if ( showMonthModal && !dropMonthMenuRef.current?.contains(e.target as Node) && !dropMonthMenuBtnRef.current?.contains(e.target as Node) ) setShowMonthModal(false); }; document.addEventListener("click", handleClickOutsideClose); return () => document.removeEventListener("click", handleClickOutsideClose); }, [showMonthModal]); const matchExceptMonth = (select: string) => { return exceptMonth.some((exceptMonth) => exceptMonth.monthEng === select); }; return ( <> ... <div> <div > <span> {mathMonth.length > 0 ? mathMonth.length : 0} posts in{" "} {monthList.engMonth} </span> <div> <span onClick={toggleMonthList} ref={dropMonthMenuBtnRef} > {yearList}. {monthList.engMonth} </span> {showMonthModal && ( <div ref={dropMonthMenuRef}> <ul> {years.map((year) => ( <li key={year} onClick={() => { if (matchExceptMonth(monthList.engMonth)) { if (year !== 2023) { setYearList(year); } } else { setYearList(year); } }} className={cls( yearList === year ? "month-selected" : "", matchExceptMonth(monthList.engMonth) && year === 2023 ? "month-disabled" : "month-noneDisabled", "month-base" )} > {year} </li> ))} </ul> <ul className="h-48 overflow-y-auto scrollbar-none"> {yearList === 2023 ? engMonthName.slice(8).map((mon) => ( <li key={mon.monthEng} onClick={() => { if ( !( yearList === 2023 && matchExceptMonth(mon.monthEng) ) ) { selectMonth(mon.monthEng, mon.monthNum); } }} className={cls( monthList.engMonth === mon.monthEng ? "month-selected" : "month-noneDisabled", "month-base" )} > {mon.monthEng} </li> )) : engMonthName.map((mon) => ( <li key={mon.monthEng} onClick={() => { if ( !( yearList === 2023 && matchExceptMonth(mon.monthEng) ) ) { selectMonth(mon.monthEng, mon.monthNum); } }} className={cls( monthList.engMonth === mon.monthEng ? "month-selected" : "month-noneDisabled", "month-base" )} > {mon.monthEng} </li> ))} </ul> </div> )} </div> <span>{blogs.results.length} total posts</span> </div> <div className="mt-2 h-full w-full"> <PostHeatMap blogs={blogs} year={yearList} month={monthList.numMonth} /> </div> </div> ... </> ); }
최종 결과