@@ -2,11 +2,11 @@ import { onSchedule } from "firebase-functions/v2/scheduler";
22import { getFunctions } from "firebase-admin/functions" ;
33import * as admin from "firebase-admin" ;
44import * as logger from "firebase-functions/logger" ;
5+ import { resolveTimeZone } from "./shared" ;
56
67const LOCATION = "asia-northeast3" ;
78const DEFAULT_HOUR = 9 ;
89const DEFAULT_MINUTE = 0 ;
9- const DEFAULT_TIMEZONE = "UTC" ;
1010const MINUTE_INTERVAL = 5 ;
1111
1212type ZonedDateParts = {
@@ -58,20 +58,22 @@ export const scheduleTodoReminder = onSchedule({
5858 continue ;
5959 }
6060 const settings = settingsDoc . data ( ) ;
61- if ( ! settings || settings . allowPushNotification !== true ) {
62- continue ;
63- }
64-
65- const hour = Number . isInteger ( settings . pushNotificationHour ) ?
66- settings . pushNotificationHour :
67- DEFAULT_HOUR ;
68- const minute = normalizeMinute ( settings . pushNotificationMinute ) ;
61+ if ( ! settings || settings . allowPushNotification !== true ) { continue ; }
62+
63+ const hour = Number . isInteger ( settings . pushNotificationHour ) ? settings . pushNotificationHour : DEFAULT_HOUR ;
64+ const configuredMinute = Number . isInteger ( settings . pushNotificationMinute ) ?
65+ Number ( settings . pushNotificationMinute ) :
66+ DEFAULT_MINUTE ;
67+ const minute = configuredMinute < 0 || configuredMinute > 59 ?
68+ DEFAULT_MINUTE :
69+ configuredMinute - ( configuredMinute % MINUTE_INTERVAL ) ;
70+
6971 const timeZone = resolveTimeZone ( settings ) ;
7072
7173 const localNow = getZonedParts ( now , timeZone ) ;
72- if ( ! isWithinNotificationWindow ( localNow , hour , minute ) ) {
73- continue ;
74- }
74+ if ( localNow . hour !== hour ) { continue ; }
75+ const windowEnd = Math . min ( minute + MINUTE_INTERVAL , 60 ) ;
76+ if ( localNow . minute < minute || localNow . minute >= windowEnd ) { continue ; }
7577
7678 const tomorrow = addDays ( localNow . year , localNow . month , localNow . day , 1 ) ;
7779 const dayAfterTomorrow = addDays ( localNow . year , localNow . month , localNow . day , 2 ) ;
@@ -90,19 +92,18 @@ export const scheduleTodoReminder = onSchedule({
9092 timeZone
9193 ) ;
9294
93- const dueDateKey = formatDateKey ( startUTC , timeZone ) ;
95+ const dueDateKey = ` ${ tomorrow . year } - ${ tomorrow . month . toString ( ) . padStart ( 2 , "0" ) } - ${ tomorrow . day . toString ( ) . padStart ( 2 , "0" ) } ` ;
9496 let todosSnapshot : FirebaseFirestore . QuerySnapshot < FirebaseFirestore . DocumentData > ;
9597 try {
9698 todosSnapshot = await admin . firestore ( )
9799 . collection ( `users/${ userId } /todoLists` )
98- . where ( "isCompleted" , "==" , false )
99100 . where ( "dueDate" , ">=" , admin . firestore . Timestamp . fromDate ( startUTC ) )
100101 . where ( "dueDate" , "<" , admin . firestore . Timestamp . fromDate ( endUTC ) )
101102 . get ( ) ;
102103 } catch ( error ) {
103104 logger . error ( "todoLists 조회 실패" , {
104105 userId,
105- at : "todoLists.where(isCompleted==false).where( dueDate>=start).where(dueDate<end)" ,
106+ at : "todoLists.where(dueDate>=start).where(dueDate<end)" ,
106107 startUTC : startUTC . toISOString ( ) ,
107108 endUTC : endUTC . toISOString ( ) ,
108109 dueDateKey,
@@ -197,20 +198,6 @@ function getZonedParts(date: Date, timeZone: string): ZonedDateParts {
197198 } ;
198199}
199200
200- function formatDateKey ( date : Date , timeZone : string ) : string {
201- const parts = new Intl . DateTimeFormat ( "en-CA" , {
202- timeZone,
203- year : "numeric" ,
204- month : "2-digit" ,
205- day : "2-digit"
206- } ) . formatToParts ( date ) ;
207-
208- const year = parts . find ( ( part ) => part . type === "year" ) ?. value ?? "1970" ;
209- const month = parts . find ( ( part ) => part . type === "month" ) ?. value ?? "01" ;
210- const day = parts . find ( ( part ) => part . type === "day" ) ?. value ?? "01" ;
211- return `${ year } -${ month } -${ day } ` ;
212- }
213-
214201function parseShortOffsetToMinutes ( shortOffset : string ) : number {
215202 if ( shortOffset === "GMT" || shortOffset === "UTC" ) return 0 ;
216203 const match = shortOffset . match ( / ^ G M T ( [ + - ] ) ( \d { 1 , 2 } ) (?: : ( \d { 2 } ) ) ? $ / ) ;
@@ -265,32 +252,3 @@ function addDays(year: number, month: number, day: number, value: number): {
265252 day : utcDate . getUTCDate ( )
266253 } ;
267254}
268-
269- function normalizeMinute ( value : unknown ) : number {
270- if ( ! Number . isInteger ( value ) ) return DEFAULT_MINUTE ;
271- const minute = Number ( value ) ;
272- if ( minute < 0 || minute > 59 ) return DEFAULT_MINUTE ;
273- return minute - ( minute % MINUTE_INTERVAL ) ;
274- }
275-
276- function isWithinNotificationWindow (
277- localNow : ZonedDateParts ,
278- configuredHour : number ,
279- configuredMinute : number
280- ) : boolean {
281- if ( localNow . hour !== configuredHour ) return false ;
282- const windowStart = configuredMinute ;
283- const windowEnd = Math . min ( configuredMinute + MINUTE_INTERVAL , 60 ) ;
284- return localNow . minute >= windowStart && localNow . minute < windowEnd ;
285- }
286-
287- function resolveTimeZone ( settings : FirebaseFirestore . DocumentData | undefined ) : string {
288- const candidate = settings ?. timeZone ?? settings ?. timezone ?? settings ?. region ;
289- if ( typeof candidate !== "string" || ! candidate . trim ( ) ) return DEFAULT_TIMEZONE ;
290- try {
291- new Intl . DateTimeFormat ( "en-US" , { timeZone : candidate } ) . format ( new Date ( ) ) ;
292- return candidate ;
293- } catch {
294- return DEFAULT_TIMEZONE ;
295- }
296- }
0 commit comments