Skip to content

Commit f5887e7

Browse files
committed
feat: 잔디그래프 UI 구현
1 parent 7170ed2 commit f5887e7

6 files changed

Lines changed: 172 additions & 7 deletions

File tree

app/globals.css

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,33 @@
4242
/* 텍스트(흰색) */
4343
--color-text-invert: #ffffff;
4444
}
45+
/* --- 잔디그래프 --- */
46+
.react-calendar-heatmap {
47+
transform-origin: top left;
48+
}
49+
50+
.react-calendar-heatmap text {
51+
font-size: 8px;
52+
fill: #9ca3af; /* gray-400 */
53+
}
54+
55+
.react-calendar-heatmap rect {
56+
rx: 3px;
57+
ry: 3px;
58+
}
59+
60+
.react-calendar-heatmap .grass-empty {
61+
fill: #f1f5f9;
62+
}
63+
.react-calendar-heatmap .grass-1 {
64+
fill: #a7f3d0;
65+
}
66+
.react-calendar-heatmap .grass-2 {
67+
fill: #34d399;
68+
}
69+
.react-calendar-heatmap .grass-3 {
70+
fill: #059669;
71+
}
72+
.react-calendar-heatmap .grass-4 {
73+
fill: #065f46;
74+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
'use client';
2+
3+
import { useMemo } from 'react';
4+
import CalendarHeatmap from 'react-calendar-heatmap';
5+
import 'react-calendar-heatmap/dist/styles.css';
6+
7+
type HeatmapValue = {
8+
date: string;
9+
count: number;
10+
};
11+
12+
/** 이번 주 토요일 기준으로 endDate 맞추기 (GitHub 느낌) */
13+
function endOfWeek(date = new Date()) {
14+
const d = new Date(date);
15+
const diff = 6 - d.getDay(); // 0=Sun, 6=Sat
16+
d.setDate(d.getDate() + diff);
17+
return d;
18+
}
19+
20+
/** mock data: 최근 1년 */
21+
function buildMockValues(days = 366): HeatmapValue[] {
22+
const out: HeatmapValue[] = [];
23+
const today = new Date();
24+
25+
for (let i = days - 1; i >= 0; i--) {
26+
const d = new Date(today);
27+
d.setDate(today.getDate() - i);
28+
29+
const iso = d.toISOString().slice(0, 10);
30+
31+
const r = Math.random();
32+
const count =
33+
r < 0.65 ? 0 : r < 0.85 ? 1 : r < 0.95 ? 2 : r < 0.985 ? 3 : 4;
34+
35+
if (count > 0) out.push({ date: iso, count });
36+
}
37+
38+
return out;
39+
}
40+
41+
export default function GrassHeatmap() {
42+
const endDate = useMemo(() => endOfWeek(new Date()), []);
43+
const startDate = useMemo(() => {
44+
const d = new Date(endDate);
45+
d.setFullYear(d.getFullYear() - 1);
46+
d.setDate(d.getDate() + 1);
47+
return d;
48+
}, [endDate]);
49+
50+
const values = useMemo(() => buildMockValues(366), []);
51+
52+
return (
53+
<>
54+
{/* 가로 길어질 때 대비 */}
55+
<div className="overflow-x-auto">
56+
<div className="min-w-max">
57+
<CalendarHeatmap
58+
startDate={startDate}
59+
endDate={endDate}
60+
values={values}
61+
gutterSize={2}
62+
showWeekdayLabels
63+
classForValue={(value) => {
64+
if (!value) return 'grass-empty';
65+
if (value.count >= 4) return 'grass-4';
66+
if (value.count === 3) return 'grass-3';
67+
if (value.count === 2) return 'grass-2';
68+
return 'grass-1';
69+
}}
70+
tooltipDataAttrs={(value) => {
71+
if (!value) return { 'data-tip': '기록 없음' };
72+
return { 'data-tip': `${value.date} · ${value.count}개 완료` };
73+
}}
74+
/>
75+
</div>
76+
</div>
77+
78+
{/* 범례 */}
79+
<div className="mt-4 flex items-center justify-end gap-2 text-xs text-slate-500">
80+
<span>Less</span>
81+
<span className="h-3 w-3 rounded-sm bg-slate-100" />
82+
<span className="h-3 w-3 rounded-sm bg-emerald-200" />
83+
<span className="h-3 w-3 rounded-sm bg-emerald-400" />
84+
<span className="h-3 w-3 rounded-sm bg-emerald-600" />
85+
<span className="h-3 w-3 rounded-sm bg-emerald-800" />
86+
<span>More</span>
87+
</div>
88+
</>
89+
);
90+
}

components/home/GraphSection.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Card from './Card';
2+
import GrassHeatmap from '../heatmap/GrassHeatmap';
23

34
interface GraphSectionProps {
45
className?: string;
@@ -7,7 +8,9 @@ interface GraphSectionProps {
78
const GraphSection = ({ className }: GraphSectionProps) => {
89
return (
910
<div className={className}>
10-
<Card title="학습 기록">잔디그래프 구현 예정</Card>
11+
<Card title="학습 기록">
12+
<GrassHeatmap />
13+
</Card>
1114
</div>
1215
);
1316
};

package-lock.json

Lines changed: 20 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
"lint": "eslint"
1010
},
1111
"dependencies": {
12-
"@radix-ui/react-popover": "^1.1.15",
1312
"@headlessui/react": "^2.2.9",
13+
"@radix-ui/react-popover": "^1.1.15",
1414
"@uiw/react-md-editor": "^4.0.11",
1515
"clsx": "^2.1.1",
1616
"date-fns": "^4.1.0",
@@ -19,6 +19,7 @@
1919
"lucide-react": "^0.562.0",
2020
"next": "16.1.1",
2121
"react": "19.2.3",
22+
"react-calendar-heatmap": "^1.10.0",
2223
"react-day-picker": "^9.13.0",
2324
"react-dom": "19.2.3",
2425
"react-icons": "^5.5.0"

types/react-calendar-heatmap.d.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
declare module 'react-calendar-heatmap' {
2+
import * as React from 'react';
3+
4+
export type HeatmapValue = {
5+
date: string;
6+
count: number;
7+
} & Record<string, unknown>;
8+
9+
export interface CalendarHeatmapProps {
10+
startDate: Date;
11+
endDate: Date;
12+
values: HeatmapValue[];
13+
14+
classForValue?: (value: HeatmapValue | null) => string;
15+
tooltipDataAttrs?: (value: HeatmapValue | null) => Record<string, string>;
16+
17+
showWeekdayLabels?: boolean;
18+
showMonthLabels?: boolean;
19+
gutterSize?: number;
20+
horizontal?: boolean;
21+
onClick?: (value: HeatmapValue | null) => void;
22+
}
23+
24+
const CalendarHeatmap: React.FC<CalendarHeatmapProps>;
25+
export default CalendarHeatmap;
26+
}

0 commit comments

Comments
 (0)