Skip to content

feat: 잔디 그래프 버그 수정 및 연속 심기 표시#57

Merged
moo1677 merged 3 commits into
devfrom
feature/heatmap-section
Jan 28, 2026
Merged

feat: 잔디 그래프 버그 수정 및 연속 심기 표시#57
moo1677 merged 3 commits into
devfrom
feature/heatmap-section

Conversation

@moo1677
Copy link
Copy Markdown
Contributor

@moo1677 moo1677 commented Jan 28, 2026

1) 작업한 이슈번호

51

2) 변경 요약 (What & Why)

  • 무엇을 변경했는지:
  • 변경했는지(문제/목표):

3) 스크린샷/동영상 (UI 변경 시)

전/후 비교, 반응형(모바일/데스크톱) 캡쳐

  • Before:
  • After:

4) 상세 변경사항

  • 라우팅/페이지:
  • 컴포넌트:
  • 상태관리:
  • API 호출:
  • 스타일:
  • 기타:

5) 참고사항

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 항목에 날짜 기준 연계 추가로 날짜별 집계가 정확해짐
    • 연속일수(streak) 계산 및 노출 로직 추가
  • Refactor

    • 일일 통계 재계산 방식으로 전환하여 집계 정확성 향상
    • 히트맵 툴팁에서 '계획 완료(Plan)' 카운트로 명칭·표시 업데이트

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 28, 2026

📝 Walkthrough

Walkthrough

일일 통계 필드명 todoDoneCountplanDoneCount로 교체하고, 통계 갱신 방식 bumpDailyStat을 전체 재계산 recomputeDailyStat으로 대체했습니다. Plan/TIL에 KST 기반 dateKey 필드를 추가하여 날짜별 집계와 연동하도록 변경했습니다.

Changes

Cohort / File(s) 요약
Heatmap UI
components/heatmap/GrassHeatmap.tsx
툴팁 및 state/맵에서 todoDoneCountplanDoneCount로 필드명 변경
일일 통계 서비스
services/heatmap/dailyStat.service.ts
bumpDailyStat 제거 및 recomputeDailyStat(uid) 추가. DailyStat 타입 확장(tilCount, planDoneCount, total, updatedAt?). fetch 함수들 반환형·매핑을 새 필드명으로 업데이트. 이전 트랜잭션 로직(runTransaction 등) 제거, setDoc/serverTimestamp 사용
계획(Plan) 서비스
services/plans/planManageService.service.ts
PlanItemdateKey?: string 추가. 항목 생성/업데이트 시 KST 기반 dateKey 계산 및 저장. 상태 토글 후 recomputeDailyStat(uid) 호출로 대체
TIL 작성 서비스
services/write/til.service.ts
TilData/TildateKey 필드 추가 및 생성/삭제 시 dateKey 설정/전파. TIL 생성·삭제 흐름에서 recomputeDailyStat(uid) 호출로 대체
타입 정의
types/til.ts
TilDocdateKey: string 필드 추가

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)
Loading

Estimated code review effort

🎯 3 (중간) | ⏱️ ~25분

Possibly related PRs

개요

일일 통계 시스템을 필드명 변경(todoDoneCountplanDoneCount)과 로직 교체(bumpDailyStatrecomputeDailyStat)로 재구성하고, 계획 항목 및 TIL에 KST 기반 dateKey 필드를 추가하여 날짜 추적 기능을 강화했습니다.

변경 사항

내용 / 파일 요약
GrassHeatmap 컴포넌트
components/heatmap/GrassHeatmap.tsx
todoDoneCount 필드를 planDoneCount로 이름 변경. 상태 타입, byDate 맵, 툴팁 계산 로직에서 필드명 업데이트
일일 통계 서비스
services/heatmap/dailyStat.service.ts
bumpDailyStat 함수를 새로운 recomputeDailyStat 함수로 교체. DailyStat 타입 확장 (tilCount, planDoneCount, total, updatedAt? 포함). DailyStatDetail 타입 제거. 함수 반환값을 새 타입 구조에 맞게 업데이트
계획 관리 서비스
services/plans/planManageService.service.ts
일일 통계 헬퍼 함수 변경 (bumpDailyStatrecomputeDailyStat). PlanItem 타입에 dateKey?: string 필드 추가. 항목 생성/업데이트 시 KST 기반 dateKey 계산 및 저장 로직 추가
TIL 작성 서비스
services/write/til.service.ts
일일 통계 헬퍼 함수 변경 (bumpDailyStatrecomputeDailyStat). TilData 타입에 dateKey: string 필드 추가. TIL 생성/삭제 시 새 필드 처리 로직 추가
TIL 타입 정의
types/til.ts
TilDoc 타입에 dateKey: string 필드 추가

코드 리뷰 예상 난이도

🎯 3 (중간) | ⏱️ ~25분

관련 가능성 있는 PR

  • feat: 잔디그래프 구현 #52: 동일한 heatmap/일일-통계 흐름을 수정하며, todoDoneCountplanDoneCount 이름 변경 및 bumpDailyStatrecomputeDailyStat 교체와 같은 내용으로 겹침
  • feat: TIL 관리 페이지 구현 #43: TIL 타입 및 서비스 변경과 연관되어 dateKey 추가 및 TIL CRUD 영역과 코드 레벨 연관성 있음

토끼의 찬사 시

🐰✨ KST로 날짜를 새긴다네,
계획과 TIL이 손을 잡고,
통계는 다시 재계산되네.
풀밭은 더 환히 빛나고,
토끼는 깡충, 코드도 깡충 🥕🌱

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning PR 설명이 템플릿 구조만 제공하고 실질적인 내용이 거의 채워져 있지 않습니다. 문제점, 변경 세부사항, 스크린샷 등 필수 정보들이 누락되어 있습니다. 변경 요약(무엇을/왜), 상세 변경사항(컴포넌트, API 호출, 기타), 스크린샷 또는 동영상을 추가하여 PR의 의도와 영향을 명확하게 설명해주세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 변경사항의 주요 내용을 적절히 요약하고 있습니다. 잔디 그래프 버그 수정과 연속 심기 표시라는 두 가지 핵심 변경사항을 명확하게 전달합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 후 일일 통계 재계산이 누락되었습니다.

toggleItemStatusdeleteTil에서는 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.tsplanManageService.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: 변수명과 표시 라벨 간의 불일치가 있습니다.

변수명 todoplanDoneCount를 참조하고 있어 혼란스러울 수 있습니다. 일관성을 위해 변수명을 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 형식
-};
-

Comment on lines +58 to +59
const dateKey = raw.dateKey;
if (typeof dateKey !== 'string') return null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

기존 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.

Suggested change
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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +30 to +67
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 }
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.ts" -o -name "*.js" | head -20

Repository: DeveloperBlog-Devflow/devflow

Length of output: 639


🏁 Script executed:

fd -e ts -e js | grep -E "(streak|Streak)" | head -20

Repository: DeveloperBlog-Devflow/devflow

Length of output: 55


🏁 Script executed:

rg "fetchStreakDays" -t ts -t js -A 5

Repository: DeveloperBlog-Devflow/devflow

Length of output: 885


🏁 Script executed:

fd "dailyStat" -e ts -e js

Repository: DeveloperBlog-Devflow/devflow

Length of output: 110


🏁 Script executed:

cat -n services/heatmap/dailyStat.service.ts

Repository: 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.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant