feat: 잔디 그래프 버그 수정 및 연속 심기 표시#57
Conversation
📝 WalkthroughWalkthrough일일 통계 필드명 Changes
Sequence Diagram(s)sequenceDiagram
participant PlanSvc as PlanService
participant TilSvc as TilService
participant DailySvc as DailyStatService
participant Firestore as Firestore
Note over PlanSvc,TilSvc: 사용자가 Plan/TIL을 생성·토글·삭제
PlanSvc->>Firestore: PlanItem 작성/업데이트 (dateKey 포함)
TilSvc->>Firestore: Til 작성/삭제 (dateKey 포함)
PlanSvc->>DailySvc: recomputeDailyStat(uid)
TilSvc->>DailySvc: recomputeDailyStat(uid)
DailySvc->>Firestore: 읽기(PlanItems, TILs by dateKey) / 쓰기(DailyStat setDoc, updatedAt)
Firestore-->>DailySvc: 최신 집계 데이터 응답
DailySvc-->>Firestore: DailyStat 저장 (tilCount, planDoneCount, total, updatedAt)
Estimated code review effort🎯 3 (중간) | ⏱️ ~25분 Possibly related PRs
개요일일 통계 시스템을 필드명 변경( 변경 사항
코드 리뷰 예상 난이도🎯 3 (중간) | ⏱️ ~25분 관련 가능성 있는 PR
토끼의 찬사 시
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
services/plans/planManageService.service.ts (1)
147-151:deletePlanItem후 일일 통계 재계산이 누락되었습니다.
toggleItemStatus와deleteTil에서는recomputeDailyStat를 호출하지만,deletePlanItem에서는 호출하지 않습니다. 완료된 플랜 항목을 삭제하면 일일 통계가 실제 데이터와 불일치하게 됩니다.🐛 제안: recomputeDailyStat 호출 추가
export const deletePlanItem = async (uid: string, itemId: string) => { const itemRef = doc(db, 'users', uid, 'planItems', itemId); await deleteDoc(itemRef); + await recomputeDailyStat(uid); };
🤖 Fix all issues with AI agents
In `@services/write/til.service.ts`:
- Around line 58-59: The current check returns null when raw.dateKey is missing,
breaking backward compatibility; change the handling in til.service.ts so
dateKey is optional: make dateKey mutable (replace "const dateKey = raw.dateKey"
with a let), and if typeof raw.dateKey !== 'string' assign a sensible fallback
(e.g., derive from raw.createdAt or raw.date, or use an empty string/undefined)
instead of returning null, and ensure downstream code that consumes dateKey (the
same function/method) can handle the fallback value so older TIL documents
remain readable.
🧹 Nitpick comments (3)
services/write/til.service.ts (1)
15-18:dateKeyKST함수가 여러 파일에 중복 정의되어 있습니다.이 함수는
dailyStat.service.ts와planManageService.service.ts에도 동일하게 정의되어 있습니다. 공통 유틸리티 모듈(예:utils/date.ts)로 추출하여 중복을 제거하는 것을 권장합니다.♻️ 제안: 공통 유틸리티로 추출
// utils/date.ts export function dateKeyKST(date = new Date()): string { const kst = new Date(date.getTime() + 9 * 60 * 60 * 1000); return kst.toISOString().slice(0, 10); }components/heatmap/GrassHeatmap.tsx (1)
83-98: 변수명과 표시 라벨 간의 불일치가 있습니다.변수명
todo가planDoneCount를 참조하고 있어 혼란스러울 수 있습니다. 일관성을 위해 변수명을plan또는planDone으로 변경하는 것을 권장합니다.♻️ 제안: 변수명 일관성 개선
const til = d?.tilCount ?? 0; - const todo = d?.planDoneCount ?? 0; + const plan = d?.planDoneCount ?? 0; const total = d?.total ?? 0; return { 'data-tooltip-id': 'grass-tip', // HTML 툴팁 (작은 박스) 'data-tooltip-html': total === 0 ? `<div style="font-size:12px"><b>${value.date}</b><br/>기록 없음</div>` : `<div style="font-size:12px"> <b>${value.date}</b><br/> 📘 TIL: ${til}개<br/> - ✅ Plan: ${todo}개<br/> + ✅ Plan: ${plan}개<br/> 🔥 합계: ${total}개 </div>`, };services/heatmap/dailyStat.service.ts (1)
28-32:formatDate함수가 사용되지 않습니다.이 함수는 파일 내에서 호출되는 곳이 없습니다. 불필요한 코드라면 제거하거나, 사용할 계획이 있다면 주석으로 의도를 명시해 주세요.
♻️ 제안: 미사용 함수 제거
-const formatDate = (d: string | Date | undefined) => { - if (!d) return ''; - if (typeof d === 'string') return d; - return d.toISOString().split('T')[0]; // YYYY-MM-DD 형식 -}; -
| const dateKey = raw.dateKey; | ||
| if (typeof dateKey !== 'string') return null; |
There was a problem hiding this comment.
기존 TIL 데이터와의 하위 호환성 문제가 있습니다.
dateKey가 없는 기존 TIL 문서는 이 검증 로직에서 null을 반환하게 되어 조회가 불가능해집니다. 기존 데이터를 위해 dateKey가 없는 경우 기본값을 사용하거나 선택적 필드로 처리해야 합니다.
🐛 제안: 하위 호환성 유지
const dateKey = raw.dateKey;
- if (typeof dateKey !== 'string') return null;
+ const validDateKey = typeof dateKey === 'string' ? dateKey : '';
const createdAt = raw.createdAt instanceof Timestamp ? raw.createdAt : null;
const updatedAt = raw.updatedAt instanceof Timestamp ? raw.updatedAt : null;
- return { title, content, dateKey, createdAt, updatedAt };
+ return { title, content, dateKey: validDateKey, createdAt, updatedAt };📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const dateKey = raw.dateKey; | |
| if (typeof dateKey !== 'string') return null; | |
| const dateKey = raw.dateKey; | |
| const validDateKey = typeof dateKey === 'string' ? dateKey : ''; | |
| const createdAt = raw.createdAt instanceof Timestamp ? raw.createdAt : null; | |
| const updatedAt = raw.updatedAt instanceof Timestamp ? raw.updatedAt : null; | |
| return { title, content, dateKey: validDateKey, createdAt, updatedAt }; |
🤖 Prompt for AI Agents
In `@services/write/til.service.ts` around lines 58 - 59, The current check
returns null when raw.dateKey is missing, breaking backward compatibility;
change the handling in til.service.ts so dateKey is optional: make dateKey
mutable (replace "const dateKey = raw.dateKey" with a let), and if typeof
raw.dateKey !== 'string' assign a sensible fallback (e.g., derive from
raw.createdAt or raw.date, or use an empty string/undefined) instead of
returning null, and ensure downstream code that consumes dateKey (the same
function/method) can handle the fallback value so older TIL documents remain
readable.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@services/heatmap/dailyStat.service.ts`:
- Around line 30-67: The recomputeDailyStat function currently calls
fetchStreakDays before today's stats are persisted, causing today's total to be
excluded; change the order so you first persist the daily stat via setDoc(ref, {
date, tilCount, planDoneCount, total, updatedAt }, { merge: true }) and only
after that call fetchStreakDays(uid) and updateDoc(doc(db, 'users', uid), {
streakDays }) to ensure the streak calculation includes today's saved total;
keep using the same ref, setDoc, fetchStreakDays, and updateDoc symbols when
reordering.
| export const recomputeDailyStat = async (uid: string) => { | ||
| const dateKey = dateKeyKST(); | ||
|
|
||
| /** 1. 오늘 완료된 planItems */ | ||
|
|
||
| const planItemsRef = collection(db, 'users', uid, 'planItems'); | ||
| const planQuery = query( | ||
| planItemsRef, | ||
| where('dateKey', '==', dateKey), | ||
| where('isChecked', '==', true) | ||
| ); | ||
| const planSnap = await getDocs(planQuery); | ||
| const planDoneCount = planSnap.size; | ||
|
|
||
| /** 2. 오늘 작성한 TIL */ | ||
| const tilRef = collection(db, 'users', uid, 'tils'); | ||
| const tilQuery = query(tilRef, where('dateKey', '==', dateKey)); | ||
| const tilSnap = await getDocs(tilQuery); | ||
| const tilCount = tilSnap.size; | ||
|
|
||
| /** 3. DailyStat 덮어쓰기 */ | ||
| const ref = doc(db, 'users', uid, 'dailyStats', dateKey); | ||
|
|
||
| /** 4. 연속 잔디 심기 일 수 계산 */ | ||
| const streakDays = await fetchStreakDays(uid); | ||
| await updateDoc(doc(db, 'users', uid), { streakDays }); | ||
|
|
||
| await setDoc( | ||
| ref, | ||
| { | ||
| date: dateKey, | ||
| tilCount, | ||
| planDoneCount, | ||
| total: tilCount + planDoneCount, | ||
| updatedAt: serverTimestamp(), | ||
| }, | ||
| { merge: true } | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "*.ts" -o -name "*.js" | head -20Repository: DeveloperBlog-Devflow/devflow
Length of output: 639
🏁 Script executed:
fd -e ts -e js | grep -E "(streak|Streak)" | head -20Repository: DeveloperBlog-Devflow/devflow
Length of output: 55
🏁 Script executed:
rg "fetchStreakDays" -t ts -t js -A 5Repository: DeveloperBlog-Devflow/devflow
Length of output: 885
🏁 Script executed:
fd "dailyStat" -e ts -e jsRepository: DeveloperBlog-Devflow/devflow
Length of output: 110
🏁 Script executed:
cat -n services/heatmap/dailyStat.service.tsRepository: DeveloperBlog-Devflow/devflow
Length of output: 4750
현재 통계가 스트릭 계산에 반영되지 않고 있습니다.
fetchStreakDays는 먼저 실행되지만, 이 시점에 오늘의 통계는 아직 데이터베이스에 저장되지 않았습니다. 따라서 새로운 날 첫 완료/작성 시 오늘의 total이 0으로 계산되어 어제부터 스트릭이 계산됩니다. 통계를 먼저 저장한 뒤 스트릭을 계산하세요.
🛠️ 제안 수정
- /** 4. 연속 잔디 심기 일 수 계산 */
- const streakDays = await fetchStreakDays(uid);
- await updateDoc(doc(db, 'users', uid), { streakDays });
-
await setDoc(
ref,
{
date: dateKey,
tilCount,
planDoneCount,
total: tilCount + planDoneCount,
updatedAt: serverTimestamp(),
},
{ merge: true }
);
+
+ /** 4. 연속 잔디 심기 일 수 계산 */
+ const streakDays = await fetchStreakDays(uid);
+ await updateDoc(doc(db, 'users', uid), { streakDays });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const recomputeDailyStat = async (uid: string) => { | |
| const dateKey = dateKeyKST(); | |
| /** 1. 오늘 완료된 planItems */ | |
| const planItemsRef = collection(db, 'users', uid, 'planItems'); | |
| const planQuery = query( | |
| planItemsRef, | |
| where('dateKey', '==', dateKey), | |
| where('isChecked', '==', true) | |
| ); | |
| const planSnap = await getDocs(planQuery); | |
| const planDoneCount = planSnap.size; | |
| /** 2. 오늘 작성한 TIL */ | |
| const tilRef = collection(db, 'users', uid, 'tils'); | |
| const tilQuery = query(tilRef, where('dateKey', '==', dateKey)); | |
| const tilSnap = await getDocs(tilQuery); | |
| const tilCount = tilSnap.size; | |
| /** 3. DailyStat 덮어쓰기 */ | |
| const ref = doc(db, 'users', uid, 'dailyStats', dateKey); | |
| /** 4. 연속 잔디 심기 일 수 계산 */ | |
| const streakDays = await fetchStreakDays(uid); | |
| await updateDoc(doc(db, 'users', uid), { streakDays }); | |
| await setDoc( | |
| ref, | |
| { | |
| date: dateKey, | |
| tilCount, | |
| planDoneCount, | |
| total: tilCount + planDoneCount, | |
| updatedAt: serverTimestamp(), | |
| }, | |
| { merge: true } | |
| ); | |
| export const recomputeDailyStat = async (uid: string) => { | |
| const dateKey = dateKeyKST(); | |
| /** 1. 오늘 완료된 planItems */ | |
| const planItemsRef = collection(db, 'users', uid, 'planItems'); | |
| const planQuery = query( | |
| planItemsRef, | |
| where('dateKey', '==', dateKey), | |
| where('isChecked', '==', true) | |
| ); | |
| const planSnap = await getDocs(planQuery); | |
| const planDoneCount = planSnap.size; | |
| /** 2. 오늘 작성한 TIL */ | |
| const tilRef = collection(db, 'users', uid, 'tils'); | |
| const tilQuery = query(tilRef, where('dateKey', '==', dateKey)); | |
| const tilSnap = await getDocs(tilQuery); | |
| const tilCount = tilSnap.size; | |
| /** 3. DailyStat 덮어쓰기 */ | |
| const ref = doc(db, 'users', uid, 'dailyStats', dateKey); | |
| await setDoc( | |
| ref, | |
| { | |
| date: dateKey, | |
| tilCount, | |
| planDoneCount, | |
| total: tilCount + planDoneCount, | |
| updatedAt: serverTimestamp(), | |
| }, | |
| { merge: true } | |
| ); | |
| /** 4. 연속 잔디 심기 일 수 계산 */ | |
| const streakDays = await fetchStreakDays(uid); | |
| await updateDoc(doc(db, 'users', uid), { streakDays }); |
🤖 Prompt for AI Agents
In `@services/heatmap/dailyStat.service.ts` around lines 30 - 67, The
recomputeDailyStat function currently calls fetchStreakDays before today's stats
are persisted, causing today's total to be excluded; change the order so you
first persist the daily stat via setDoc(ref, { date, tilCount, planDoneCount,
total, updatedAt }, { merge: true }) and only after that call
fetchStreakDays(uid) and updateDoc(doc(db, 'users', uid), { streakDays }) to
ensure the streak calculation includes today's saved total; keep using the same
ref, setDoc, fetchStreakDays, and updateDoc symbols when reordering.
1) 작업한 이슈번호
51
2) 변경 요약 (What & Why)
3) 스크린샷/동영상 (UI 변경 시)
4) 상세 변경사항
5) 참고사항
Summary by CodeRabbit
릴리스 노트
New Features
Refactor
✏️ Tip: You can customize this high-level summary in your review settings.