프로젝트/웹페이지
웹 개발 1도 모르는 사람이 웹페이지 만들기 프로젝트 10일차
쪼르뚜
2025. 1. 20. 21:35
728x90
반응형

오늘은 깃허브 잔디처럼 월별 잔디를 만들어보겠습니다. 근데 알록달록한...
👩💻 코드 리팩토링
오늘 작업하기에 앞서 점점 거대해지는 page.js와 main-client.js 코드를 리팩토링했습니다.
우선 page.js에서 데이터 요청을 분리하여 가독성과 재사용성을 높였습니다.
// 노션 멤버 DB 가져오기
const fetchMemberData = async () => {
try {
const response = await notion.databases.query({ database_id: memberDatabaseId });
return response.results.map((page) => ({
memberId: page.id,
memberName: page.properties["이름"]?.title[0]?.text?.content || "Unknown",
goal: page.properties["목표"]?.select?.name || "Unknown",
position: page.properties["직책"]?.select?.name || "Unknown",
iconUrl: page.icon?.external?.url || null,
}));
} catch (error) {
console.error("Failed to fetch member data:", error);
return [];
}
};
// 노션 활동 DB 가져오기
const fetchActivityData = async () => {
let activities = [];
let hasMore = true;
let nextCursor = null;
try {
while (hasMore) {
const response = await notion.databases.query({
database_id: activityDatabaseId,
start_cursor: nextCursor || undefined,
});
activities = [
...activities,
...response.results.map((page) => ({
memberId: page.properties["멤버 DB"]?.relation[0]?.id || null,
time: page.properties["시간"]?.number || 0,
state: page.properties["상태"]?.select?.name || "좋음",
rollupDate: page.properties["날짜"]?.rollup?.array[0]?.date?.start || null,
})),
];
hasMore = response.has_more;
nextCursor = response.next_cursor;
}
} catch (error) {
console.error("Failed to fetch activity data:", error);
}
return activities;
};
데이터 가공 부분도 따로 분리하였고, 로직을 단순화시켰습니다.
const processData = (members, activities) => {
const memberMap = Object.fromEntries(members.map((m) => [m.memberId, m]));
const activitySummary = {};
activities.forEach(({ memberId, time, state, rollupDate }) => {
if (!rollupDate || !memberMap[memberId]) return;
const date = new Date(rollupDate);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const monthKey = `${year}년 ${String(month).padStart(2, "0")}월`;
if (!activitySummary[memberId]) {
activitySummary[memberId] = { activityByMonth: {}, totalTime: 0 };
}
const memberActivity = activitySummary[memberId];
if (!memberActivity.activityByMonth[monthKey]) {
memberActivity.activityByMonth[monthKey] = {
totalTime: 0,
count: 0,
stateCounts: { 좋음: 0, 보통: 0, 나쁨: 0 },
activityByDate: {},
};
}
const monthActivity = memberActivity.activityByMonth[monthKey];
monthActivity.totalTime += time;
monthActivity.count += 1;
monthActivity.stateCounts[state] = (monthActivity.stateCounts[state] || 0) + 1;
monthActivity.activityByDate[day] = {
time: (monthActivity.activityByDate[day]?.time || 0) + time,
state,
};
memberActivity.totalTime += time;
});
return members.map((member) => ({
...member,
...activitySummary[member.memberId],
}));
};
const memberData = await fetchMemberData();
const activityData = await fetchActivityData();
const processedData = processData(memberData, activityData);
main-client.js도 함수와 컴포넌트를 분리했습니다.
// 현재 년도와 월 구하는 함수
function getCurrentMonth() {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, "0");
return `${year}년 ${month}월`;
}
// 멤버 카드 그리는 컴포터는 분리
function MemberCard({ memberName, iconUrl, monthData, selectedMonth }) {
const { totalTime, count, stateCounts } = monthData;
const activityByDate = monthData.activityByDate;
return (
<div key={memberName} className="p-2 lg:w-1/3 md:w-1/2 w-full">
<div className="h-full flex flex-col items-center border-gray-200 border p-4 rounded-lg">
{iconUrl && (
<img
src={iconUrl}
alt={`${memberName}의 아이콘`}
className="w-16 h-16 mb-4 rounded-full"
/>
)}
<h2 className="text-gray-900 title-font font-medium mb-2">{memberName}</h2>
<p className="text-gray-500 mb-2">
{count}일 동안 {Math.floor(totalTime)}시간
</p>
<div className="flex w-full space-x-4 items-start">
<div className="w-1/2 h-40">
<PieChart stateCounts={stateCounts} />
</div>
</div>
</div>
</div>
);
}
MainClient 코드가 간소화 됐습니다.
추후 다른 컴포넌트들도 수정할 때 분리시켜야겠습니다.
export default function MainClient({ memberData }) {
const [data, setData] = useState(null);
const [selectedMonth, setSelectedMonth] = useState(getCurrentMonth());
useEffect(() => {
if (memberData) {
setData(memberData);
}
}, [memberData]);
if (!data) {
return <div>Loading...</div>;
}
const uniqueMonths = Array.from(
new Set(
memberData.flatMap((member) => Object.keys(member.activityByMonth))
)
).sort();
return (
<section className="text-gray-500 body-font">
<div className="container px-5 py-24 mx-auto">
<div className="flex flex-col text-center w-full mb-10">
<h1 className="sm:text-3xl text-2xl font-medium title-font mb-4 text-gray-900">
짭알못 활동 기록
</h1>
<p className="lg:w-2/3 mx-auto leading-relaxed text-base">
과거에 '금요일을 알차게 보내는 건 못 참지'라는 금요일마다 자기계발을 하는 모임이 있었습니다.
저희는 그 모임과 별개로 매주 목요일마다 자기계발 모임을 하였습니다.
어느 날 누군가 '짭알못이다!'라고 외친 이후로 그렇게 불리게 되었습니다.
하지만 지금은 우리가 찐 ㅋ😎
</p>
</div>
<div className="flex justify-end items-center w-full mb-6">
<select
className="border border-gray-300 rounded-md px-4 py-2 bg-white"
value={selectedMonth}
onChange={(e) => setSelectedMonth(e.target.value)}
>
{uniqueMonths.map((month) => (
<option key={month} value={month}>
{month}
</option>
))}
</select>
</div>
<div className="flex flex-wrap -m-2">
{memberData
.map(({ memberName, iconUrl, activityByMonth }) => {
const monthData = activityByMonth[selectedMonth];
if (!monthData) return null;
return (
<MemberCard
key={memberName}
memberName={memberName}
iconUrl={iconUrl}
monthData={monthData}
selectedMonth={selectedMonth}
/>
);
})}
</div>
</div>
</section>
);
}
🌱 월 별 잔디 만들기
월 별 잔디를 만들기 위해 필요한 정보들을 구합니다.
- selectedMonth : 현재 선택된 월
- activityByDate : object로 key는 일, value에는 시간과 상태가 들어있음
- totalDays : 선택된 월의 마지막 날짜
- firstDay : 해당 월 1일의 요일 ( 0은 일요일, 1은 월요일 ... )
- dayArray : 1일부터 마지막 달까지 배열 생성
function MonthGrid({ selectedMonth, activityByDate }) {
const [days, setDays] = useState([]);
const [startDay, setStartDay] = useState(0);
useEffect(() => {
const [year, month] = selectedMonth.split("년 ").map((str) => str.replace("월", "").trim());
const totalDays = new Date(year, month, 0).getDate();
const firstDay = new Date(year, month - 1, 1).getDay();
const dayArray = Array.from({ length: totalDays }, (_, i) => i + 1);
setDays(dayArray);
setStartDay(firstDay);
}, [selectedMonth]);
}
시작 요일 이전에는 빈칸을 채우고, 해당 일의 상태에 맞게 이모지를 표시합니다.
return (
<div className="grid grid-cols-7 gap-1 text-center">
{Array.from({ length: startDay }, (_, i) => (
<div key={`empty-${i}`} className="w-6 h-6 flex items-center justify-center"></div>
))}
{days.map((day) => {
const activity = activityByDate[day]?.state;
let emoji;
switch (activity) {
case "좋음":
emoji = "🟩";
break;
case "보통":
emoji = "🟨";
break;
case "나쁨":
emoji = "🟥";
break;
default:
emoji = "⬜";
}
return (
<div key={day} className="w-6 h-6 flex items-center justify-center">
{emoji}
</div>
);
})}
</div>
);
멤버 카드를 그리는 함수에서 차트 옆에 위치시켜 줍니다.
<div className="flex w-full space-x-4 items-start">
<div className="w-1/2 h-40">
<PieChart stateCounts={stateCounts} />
</div>
<div className="w-1/2">
<MonthGrid
selectedMonth={selectedMonth}
activityByDate={activityByDate}
/>
</div>
</div>
미리 보기에서처럼 월 별 잔디를 확인할 수 있습니다🌱
월 별 잔디 추가 by JoHyeonJi0408 · Pull Request #4 · JoHyeonJi0408/Study-Introduction-Website
github.com

728x90
반응형