Skip to content

Commit 01b991b

Browse files
authored
Merge pull request #57 from DeveloperBlog-Devflow/feature/heatmap-section
feat: 잔디 그래프 버그 수정 및 연속 심기 표시
2 parents df88c84 + 1d3ff89 commit 01b991b

5 files changed

Lines changed: 132 additions & 74 deletions

File tree

components/heatmap/GrassHeatmap.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export default function GrassHeatmap({ uid }: Props) {
2727
}, [endDate]);
2828

2929
const [values, setValues] = useState<
30-
{ date: string; total: number; tilCount?: number; todoDoneCount?: number }[]
30+
{ date: string; total: number; tilCount?: number; planDoneCount?: number }[]
3131
>([]);
3232

3333
useEffect(() => {
@@ -41,12 +41,12 @@ export default function GrassHeatmap({ uid }: Props) {
4141
const byDate = useMemo(() => {
4242
const m = new Map<
4343
string,
44-
{ tilCount: number; todoDoneCount: number; total: number }
44+
{ tilCount: number; planDoneCount: number; total: number }
4545
>();
4646
for (const s of values) {
4747
m.set(s.date, {
4848
tilCount: s.tilCount ?? 0,
49-
todoDoneCount: s.todoDoneCount ?? 0,
49+
planDoneCount: s.planDoneCount ?? 0,
5050
total: s.total ?? 0,
5151
});
5252
}
@@ -81,7 +81,7 @@ export default function GrassHeatmap({ uid }: Props) {
8181
const d = byDate.get(value.date);
8282

8383
const til = d?.tilCount ?? 0;
84-
const todo = d?.todoDoneCount ?? 0;
84+
const todo = d?.planDoneCount ?? 0;
8585
const total = d?.total ?? 0;
8686

8787
return {
Lines changed: 100 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,79 @@
11
import {
22
doc,
3-
runTransaction,
4-
serverTimestamp,
3+
setDoc,
54
collection,
65
getDocs,
76
query,
87
where,
8+
serverTimestamp,
99
orderBy,
1010
getDoc,
11+
Timestamp,
12+
limit,
13+
updateDoc,
1114
} from 'firebase/firestore';
1215
import { db } from '@/lib/firebase';
1316

1417
export interface DailyStat {
1518
date: string; // YYYY-MM-DD
16-
total: number;
17-
}
18-
19-
export type DailyStatDetail = {
20-
date: string;
2119
tilCount: number;
22-
todoDoneCount: number;
20+
planDoneCount: number;
2321
total: number;
24-
};
22+
updatedAt?: Timestamp;
23+
}
2524

2625
function dateKeyKST(date = new Date()) {
2726
const kst = new Date(date.getTime() + 9 * 60 * 60 * 1000);
2827
return kst.toISOString().slice(0, 10);
2928
}
3029

31-
export const bumpDailyStat = async (
32-
uid: string,
33-
deltaTil: number,
34-
deltaTodoDone: number
35-
) => {
36-
const key = dateKeyKST();
37-
const ref = doc(db, `users/${uid}/dailyStats/${key}`);
38-
39-
await runTransaction(db, async (tx) => {
40-
const snap = await tx.get(ref);
41-
42-
const prev = snap.exists()
43-
? (snap.data() as {
44-
tilCount?: number;
45-
todoDoneCount?: number;
46-
total?: number;
47-
})
48-
: {};
49-
50-
const nextTil = Math.max(0, (prev.tilCount ?? 0) + deltaTil);
51-
const nextTodo = Math.max(0, (prev.todoDoneCount ?? 0) + deltaTodoDone);
52-
const nextTotal = Math.max(0, nextTil + nextTodo);
53-
54-
tx.set(
55-
ref,
56-
{
57-
date: key,
58-
tilCount: nextTil,
59-
todoDoneCount: nextTodo,
60-
total: nextTotal,
61-
updatedAt: serverTimestamp(),
62-
},
63-
{ merge: true }
64-
);
65-
});
30+
export const recomputeDailyStat = async (uid: string) => {
31+
const dateKey = dateKeyKST();
32+
33+
/** 1. 오늘 완료된 planItems */
34+
35+
const planItemsRef = collection(db, 'users', uid, 'planItems');
36+
const planQuery = query(
37+
planItemsRef,
38+
where('dateKey', '==', dateKey),
39+
where('isChecked', '==', true)
40+
);
41+
const planSnap = await getDocs(planQuery);
42+
const planDoneCount = planSnap.size;
43+
44+
/** 2. 오늘 작성한 TIL */
45+
const tilRef = collection(db, 'users', uid, 'tils');
46+
const tilQuery = query(tilRef, where('dateKey', '==', dateKey));
47+
const tilSnap = await getDocs(tilQuery);
48+
const tilCount = tilSnap.size;
49+
50+
/** 3. DailyStat 덮어쓰기 */
51+
const ref = doc(db, 'users', uid, 'dailyStats', dateKey);
52+
53+
await setDoc(
54+
ref,
55+
{
56+
date: dateKey,
57+
tilCount,
58+
planDoneCount,
59+
total: tilCount + planDoneCount,
60+
updatedAt: serverTimestamp(),
61+
},
62+
{ merge: true }
63+
);
64+
65+
/** 4. 연속 잔디 심기 일 수 계산 */
66+
const streakDays = await fetchStreakDays(uid);
67+
await updateDoc(doc(db, 'users', uid), { streakDays });
6668
};
6769

6870
export const fetchDailyStats = async (uid: string): Promise<DailyStat[]> => {
6971
const colRef = collection(db, 'users', uid, 'dailyStats');
7072

71-
const endDate = dateKeyKST(new Date());
72-
const startDate = (() => {
73-
const d = new Date();
74-
d.setFullYear(d.getFullYear() - 1);
75-
d.setDate(d.getDate() + 1);
76-
return dateKeyKST(d);
77-
})();
73+
const endDate = dateKeyKST();
74+
const start = new Date();
75+
start.setFullYear(start.getFullYear() - 1);
76+
const startDate = dateKeyKST(start);
7877

7978
const q = query(
8079
colRef,
@@ -85,21 +84,18 @@ export const fetchDailyStats = async (uid: string): Promise<DailyStat[]> => {
8584

8685
const snap = await getDocs(q);
8786

88-
return snap.docs.map((doc) => {
89-
const data = doc.data();
90-
return {
91-
date: data.date,
92-
tilCount: data.tilCount ?? 0,
93-
todoDoneCount: data.todoDoneCount ?? 0,
94-
total: data.total ?? 0,
95-
};
96-
});
87+
return snap.docs.map((d) => ({
88+
date: d.data().date,
89+
tilCount: d.data().tilCount ?? 0,
90+
planDoneCount: d.data().planDoneCount ?? 0,
91+
total: d.data().total ?? 0,
92+
}));
9793
};
9894

9995
export const fetchDailyStatByDate = async (
10096
uid: string,
101-
date: string // YYYY-MM-DD
102-
): Promise<DailyStatDetail | null> => {
97+
date: string
98+
): Promise<DailyStat | null> => {
10399
const ref = doc(db, 'users', uid, 'dailyStats', date);
104100
const snap = await getDoc(ref);
105101

@@ -109,7 +105,48 @@ export const fetchDailyStatByDate = async (
109105
return {
110106
date: data.date,
111107
tilCount: data.tilCount ?? 0,
112-
todoDoneCount: data.todoDoneCount ?? 0,
108+
planDoneCount: data.planDoneCount ?? 0,
113109
total: data.total ?? 0,
114110
};
115111
};
112+
113+
function prevDateKey(key: string) {
114+
const [y, m, d] = key.split('-').map(Number);
115+
// UTC로 만들고 하루 빼서 다시 YYYY-MM-DD
116+
const utc = new Date(Date.UTC(y, m - 1, d));
117+
utc.setUTCDate(utc.getUTCDate() - 1);
118+
return utc.toISOString().slice(0, 10);
119+
}
120+
121+
async function fetchStreakDays(uid: string): Promise<number> {
122+
const todayKey = dateKeyKST();
123+
124+
// 최근 400일 정도만 읽어도 충분 (1년 스트릭 기준)
125+
const statsRef = collection(db, 'users', uid, 'dailyStats');
126+
const q = query(
127+
statsRef,
128+
where('date', '<=', todayKey),
129+
orderBy('date', 'desc'),
130+
limit(400)
131+
);
132+
133+
const snap = await getDocs(q);
134+
135+
// 빠른 조회용 map
136+
const map = new Map<string, number>();
137+
snap.docs.forEach((d) => {
138+
const data = d.data() as DailyStat;
139+
map.set(data.date, data.total ?? 0);
140+
});
141+
142+
const todayTotal = map.get(todayKey) ?? 0;
143+
let cursor = todayTotal > 0 ? todayKey : prevDateKey(todayKey);
144+
145+
let streak = 0;
146+
while ((map.get(cursor) ?? 0) > 0) {
147+
streak += 1;
148+
cursor = prevDateKey(cursor);
149+
}
150+
151+
return streak;
152+
}

services/plans/planManageService.service.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
writeBatch,
1313
} from 'firebase/firestore';
1414
import { db } from '@/lib/firebase';
15-
import { bumpDailyStat } from '@/services/heatmap/dailyStat.service';
15+
import { recomputeDailyStat } from '@/services/heatmap/dailyStat.service';
1616

1717
// 플랜 데이터 타입
1818
export interface Plan {
@@ -30,6 +30,11 @@ export interface PlanItem {
3030
isChecked: boolean;
3131
deadline?: Date;
3232
createdAt: Date;
33+
dateKey?: string;
34+
}
35+
function dateKeyKST(date = new Date()) {
36+
const kst = new Date(date.getTime() + 9 * 60 * 60 * 1000);
37+
return kst.toISOString().slice(0, 10);
3338
}
3439

3540
// 1. 플랜 생성하기
@@ -99,6 +104,7 @@ export const addPlanItem = async (
99104
isChecked: false,
100105
deadline: deadline ? Timestamp.fromDate(deadline) : null,
101106
createdAt: Timestamp.now(),
107+
dateKey: deadline ? dateKeyKST(deadline) : null,
102108
});
103109
};
104110

@@ -112,8 +118,7 @@ export const toggleItemStatus = async (
112118
await updateDoc(itemRef, {
113119
isChecked: !currentStatus,
114120
});
115-
const delta = !currentStatus ? 1 : -1;
116-
await bumpDailyStat(uid, 0, delta);
121+
await recomputeDailyStat(uid);
117122
};
118123

119124
// 6. 플랜 삭제
@@ -178,6 +183,7 @@ export const updatePlanItem = async (
178183
text?: string; // 수정할 제목
179184
description?: string; // 수정할 설명
180185
deadline?: Date | null; // 수정할 마감일 (null이면 마감일 삭제)
186+
dateKey?: string;
181187
}
182188
) => {
183189
const itemRef = doc(db, 'users', uid, 'planItems', itemId);
@@ -187,6 +193,7 @@ export const updatePlanItem = async (
187193
text?: string;
188194
description?: string;
189195
deadline?: Timestamp | null;
196+
dateKey?: string | null;
190197
} = {};
191198

192199
// 1. 제목이 전달되었으면 업데이트 목록에 추가
@@ -204,6 +211,9 @@ export const updatePlanItem = async (
204211
updatePayload.deadline = updates.deadline
205212
? Timestamp.fromDate(updates.deadline)
206213
: null; // null을 넘기면 DB에서 마감일이 사라짐
214+
updatePayload.dateKey = updates.deadline
215+
? dateKeyKST(updates.deadline)
216+
: null;
207217
}
208218

209219
// 변경사항이 있을 때만 DB 요청

services/write/til.service.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,19 @@ import {
1010
getCountFromServer,
1111
} from 'firebase/firestore';
1212
import { db } from '@/lib/firebase';
13-
import { bumpDailyStat } from '@/services/heatmap/dailyStat.service';
13+
import { recomputeDailyStat } from '@/services/heatmap/dailyStat.service';
14+
15+
function dateKeyKST(date = new Date()) {
16+
const kst = new Date(date.getTime() + 9 * 60 * 60 * 1000);
17+
return kst.toISOString().slice(0, 10);
18+
}
1419

1520
export type TilData = {
1621
title: string;
1722
content: string;
1823
createdAt: Timestamp | null;
1924
updatedAt: Timestamp | null;
25+
dateKey: string;
2026
};
2127

2228
export type Til = TilData & { id: string };
@@ -26,10 +32,11 @@ export async function createTil(uid: string, content: string, title: string) {
2632
const docRef = await addDoc(tilsCol, {
2733
title: title,
2834
content,
35+
dateKey: dateKeyKST(),
2936
createdAt: serverTimestamp(),
3037
updatedAt: serverTimestamp(),
3138
});
32-
await bumpDailyStat(uid, 1, 0);
39+
await recomputeDailyStat(uid);
3340
await fetchTilCount(uid);
3441
return docRef.id;
3542
}
@@ -48,11 +55,13 @@ const parseTilData = (raw: unknown): TilData | null => {
4855
const title = raw.title;
4956
if (typeof title !== 'string') return null;
5057

51-
const createdAt = raw.createdAt instanceof Timestamp ? raw.createdAt : null;
58+
const dateKey = raw.dateKey;
59+
if (typeof dateKey !== 'string') return null;
5260

61+
const createdAt = raw.createdAt instanceof Timestamp ? raw.createdAt : null;
5362
const updatedAt = raw.updatedAt instanceof Timestamp ? raw.updatedAt : null;
5463

55-
return { title, content, createdAt, updatedAt };
64+
return { title, content, dateKey, createdAt, updatedAt };
5665
};
5766

5867
export const fetchMyTil = async (
@@ -89,6 +98,7 @@ export const updateTil = async (
8998

9099
export const deleteTil = async (uid: string, tilId: string) => {
91100
await deleteDoc(doc(db, 'users', uid, 'tils', tilId));
101+
await recomputeDailyStat(uid);
92102
await fetchTilCount(uid);
93103
};
94104

types/til.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ export type TilDoc = {
1212
content: string;
1313
createdAt: Timestamp;
1414
updatedAt: Timestamp;
15+
dateKey: string;
1516
};

0 commit comments

Comments
 (0)