Skip to content

Commit 155c3fb

Browse files
committed
feat: 잔디 클릭 시 디테일 표시
1 parent 5289d09 commit 155c3fb

4 files changed

Lines changed: 114 additions & 27 deletions

File tree

components/heatmap/GrassHeatmap.tsx

Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,21 @@
33
import { useEffect, useMemo, useState } from 'react';
44
import CalendarHeatmap from 'react-calendar-heatmap';
55
import 'react-calendar-heatmap/dist/styles.css';
6+
import { Tooltip } from 'react-tooltip';
7+
import 'react-tooltip/dist/react-tooltip.css';
68
import { fetchDailyStats } from '@/services/heatmap/dailyStat.service';
79

8-
type HeatmapValue = {
9-
date: string;
10-
count: number;
11-
};
10+
type HeatmapValue = { date: string; count: number };
1211

13-
/** 토요일 기준으로 endDate 설정 */
1412
function endOfWeek(date = new Date()) {
1513
const d = new Date(date);
16-
const diff = 6 - d.getDay(); // 0=Sun, 6=Sat
17-
d.setDate(d.getDate() + diff);
14+
d.setDate(d.getDate() + (6 - d.getDay()));
1815
return d;
1916
}
2017

21-
type Props = {
22-
uid?: string;
23-
};
18+
type Props = { uid?: string };
2419

25-
export const GrassHeatmap = ({ uid }: Props) => {
20+
export default function GrassHeatmap({ uid }: Props) {
2621
const endDate = useMemo(() => endOfWeek(new Date()), []);
2722
const startDate = useMemo(() => {
2823
const d = new Date(endDate);
@@ -31,28 +26,46 @@ export const GrassHeatmap = ({ uid }: Props) => {
3126
return d;
3227
}, [endDate]);
3328

34-
const [values, setValues] = useState<HeatmapValue[]>([]);
29+
const [values, setValues] = useState<
30+
{ date: string; total: number; tilCount?: number; todoDoneCount?: number }[]
31+
>([]);
32+
3533
useEffect(() => {
3634
if (!uid) return;
37-
3835
(async () => {
3936
const stats = await fetchDailyStats(uid);
40-
const heatmapValues: HeatmapValue[] = stats.map((s) => ({
41-
date: s.date,
42-
count: s.total,
43-
}));
44-
setValues(heatmapValues);
37+
setValues(stats);
4538
})();
4639
}, [uid]);
40+
41+
const byDate = useMemo(() => {
42+
const m = new Map<
43+
string,
44+
{ tilCount: number; todoDoneCount: number; total: number }
45+
>();
46+
for (const s of values) {
47+
m.set(s.date, {
48+
tilCount: s.tilCount ?? 0,
49+
todoDoneCount: s.todoDoneCount ?? 0,
50+
total: s.total ?? 0,
51+
});
52+
}
53+
return m;
54+
}, [values]);
55+
56+
const heatmapValues: HeatmapValue[] = useMemo(
57+
() => values.map((s) => ({ date: s.date, count: s.total ?? 0 })),
58+
[values]
59+
);
60+
4761
return (
4862
<>
49-
{/* 가로 길어질 때 대비 */}
5063
<div className="overflow-x-auto">
5164
<div className="min-w-max">
5265
<CalendarHeatmap
5366
startDate={startDate}
5467
endDate={endDate}
55-
values={values}
68+
values={heatmapValues}
5669
gutterSize={2}
5770
showWeekdayLabels
5871
classForValue={(value) => {
@@ -63,13 +76,38 @@ export const GrassHeatmap = ({ uid }: Props) => {
6376
return 'grass-1';
6477
}}
6578
tooltipDataAttrs={(value) => {
66-
if (!value) return { 'data-tip': '기록 없음' };
67-
return { 'data-tip': `${value.date} · ${value.count}개 완료` };
79+
if (!value?.date)
80+
return { 'data-tooltip-id': '', 'data-tooltip-html': '' };
81+
const d = byDate.get(value.date);
82+
83+
const til = d?.tilCount ?? 0;
84+
const todo = d?.todoDoneCount ?? 0;
85+
const total = d?.total ?? 0;
86+
87+
return {
88+
'data-tooltip-id': 'grass-tip',
89+
// HTML 툴팁 (작은 박스)
90+
'data-tooltip-html':
91+
total === 0
92+
? `<div style="font-size:12px"><b>${value.date}</b><br/>기록 없음</div>`
93+
: `<div style="font-size:12px">
94+
<b>${value.date}</b><br/>
95+
📘 TIL: ${til}개<br/>
96+
✅ Plan: ${todo}개<br/>
97+
🔥 합계: ${total}
98+
</div>`,
99+
};
68100
}}
69101
/>
70102
</div>
71103
</div>
72104

105+
{/* 툴팁 컴포넌트 */}
106+
<Tooltip
107+
id="grass-tip"
108+
place="top"
109+
className="!rounded-lg !bg-black/80 !px-3 !py-2 !text-xs !text-white"
110+
/>
73111
{/* 범례 */}
74112
<div className="mt-4 flex items-center justify-end gap-2 text-xs text-slate-500">
75113
<span>Less</span>
@@ -82,5 +120,4 @@ export const GrassHeatmap = ({ uid }: Props) => {
82120
</div>
83121
</>
84122
);
85-
};
86-
export default GrassHeatmap;
123+
}

package-lock.json

Lines changed: 22 additions & 1 deletion
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
@@ -22,7 +22,8 @@
2222
"react-calendar-heatmap": "^1.10.0",
2323
"react-day-picker": "^9.13.0",
2424
"react-dom": "19.2.3",
25-
"react-icons": "^5.5.0"
25+
"react-icons": "^5.5.0",
26+
"react-tooltip": "^5.30.0"
2627
},
2728
"devDependencies": {
2829
"@tailwindcss/postcss": "^4",

services/heatmap/dailyStat.service.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
query,
88
where,
99
orderBy,
10+
getDoc,
1011
} from 'firebase/firestore';
1112
import { db } from '@/lib/firebase';
1213

@@ -15,6 +16,13 @@ export interface DailyStat {
1516
total: number;
1617
}
1718

19+
export type DailyStatDetail = {
20+
date: string;
21+
tilCount: number;
22+
todoDoneCount: number;
23+
total: number;
24+
};
25+
1826
function dateKeyKST(date = new Date()) {
1927
const kst = new Date(date.getTime() + 9 * 60 * 60 * 1000);
2028
return kst.toISOString().slice(0, 10);
@@ -57,7 +65,7 @@ export const bumpDailyStat = async (
5765
});
5866
};
5967

60-
export const fetchDailyStats = async (uid: string) => {
68+
export const fetchDailyStats = async (uid: string): Promise<DailyStat[]> => {
6169
const colRef = collection(db, 'users', uid, 'dailyStats');
6270

6371
const endDate = dateKeyKST(new Date());
@@ -81,7 +89,27 @@ export const fetchDailyStats = async (uid: string) => {
8189
const data = doc.data();
8290
return {
8391
date: data.date,
92+
tilCount: data.tilCount ?? 0,
93+
todoDoneCount: data.todoDoneCount ?? 0,
8494
total: data.total ?? 0,
8595
};
8696
});
8797
};
98+
99+
export const fetchDailyStatByDate = async (
100+
uid: string,
101+
date: string // YYYY-MM-DD
102+
): Promise<DailyStatDetail | null> => {
103+
const ref = doc(db, 'users', uid, 'dailyStats', date);
104+
const snap = await getDoc(ref);
105+
106+
if (!snap.exists()) return null;
107+
108+
const data = snap.data();
109+
return {
110+
date: data.date,
111+
tilCount: data.tilCount ?? 0,
112+
todoDoneCount: data.todoDoneCount ?? 0,
113+
total: data.total ?? 0,
114+
};
115+
};

0 commit comments

Comments
 (0)