11import {
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' ;
1215import { db } from '@/lib/firebase' ;
1316
1417export 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
2625function 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
6870export 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
9995export 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+ }
0 commit comments