이전 글에서는 달력을 만들었다면 이번 글에서는 날짜에 해당하는 포스트 수와 Home에서 이번 달 만이 아닌 다른 달과 년도를 선택할 수 있는 모달 창과 월 별 총 포스팅 수, 달력을 만든<PostHeatMap /> 에 넘겨줄 props 를 구한다.
날짜에 해당하는 포스트 수
달력을 만들기 전에 날짜마다 포스팅 된 글 수를 구한다.
- map() 함수를 사용하여 각 포스트의 created_time을 추출하고, 이를 JavaScript의 Date 객체로 변환하여 해당 날짜를 한국 표준시(UTC+9)로 변환하고, 날짜 정보에서 시간 부분을 제외한 후 toISOString()을 통해 ISO 형식의 문자열로 변환해 split("T")[0]을 사용하여 날짜 부분 만을 추출하여 korDate에 저장한다.
- createPostCount는 날짜 별 포스트 개수를 저장하는 객체로 createPost 배열을 순회하면서 각 날짜에 대한 포스트 개수를 계산한다.
createPostCount[x] = (createPostCount[x] || 0) + 1 설명
- createPostCount[x] : 날짜(x)를 키로 하는 createPostCount 객체에서 해당 날짜의 값에 접근합니다. 만약 해당 키가 존재하지 않으면 undefined가 반환된다.
- (createPostCount|| 0) : 순회시 해당 날짜의 값을 가져오되, || 연산자를 사용하여 만약 값이 존재하지 않으면 ( = 처음 마주한 값 ) 0으로 대체합니다. 만약 createPostCount[x]가 값이 존재하면 그 값을 그대로 사용한다.
// 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);
};
return (
<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 방지했다.
관련 글
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 처럼 모달 외부 클릭 시 닫힐 수 있게 적용했다.
관련 글
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()
- 각 포스트의 created_time 속성을 추출하여 create 변수에 저장합니다.
- 해당 시간을 현재 시스템의 타임존과 UTC와의 차이를 고려하여 한국 표준시(UTC+9)로 변환하여 날짜 부분만 추출한다.
mathMonth()
- createPost 배열을 사용하여, 선택된 년도( yearList )와 월(monthList.numMonth)에 해당하는 포스트들을 필터링 해 x.slice(0, 7)를 통해 각 날짜의 연도와 월 부분과 일치하는 포스트의 작성 날짜만이 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>
...
</>
);
}
최종 결과
'React' 카테고리의 다른 글
React.memo, useMemo, useCallback역할 및 차이점 (2) | 2024.10.14 |
---|---|
달력 (히트맵) 만들기 (1) (0) | 2024.09.12 |
개별 토글 리스트 (0) | 2024.09.11 |
useform 추가 데이터 (0) | 2024.09.06 |
react-cookie (1) | 2024.09.04 |