From 50eb08e85327c6897f3e5fb0bf97963ad631e9f9 Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 2 Apr 2026 12:25:15 +0900 Subject: [PATCH 01/10] =?UTF-8?q?refactor:=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EB=AC=B8=EA=B5=AC=EA=B0=80=20=EC=A7=81=EC=A0=91=EB=B3=B4?= =?UTF-8?q?=EC=9D=B4=EB=8F=84=EB=A1=9D=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Firebase/functions/src/common/error.ts | 16 +---- Firebase/functions/src/fcm/notification.ts | 7 +- Firebase/functions/src/fcm/schedule.ts | 24 +++---- .../functions/src/notification/cleanup.ts | 14 ++-- .../functions/src/notification/deletion.ts | 17 ++--- Firebase/functions/src/todo/cleanup.ts | 69 +++++++++++++------ Firebase/functions/src/todo/deletion.ts | 17 ++--- Firebase/functions/src/todo/update.ts | 7 +- Firebase/functions/src/todoCategory/update.ts | 12 ++-- Firebase/functions/src/webPage/cleanup.ts | 8 ++- Firebase/functions/src/webPage/deletion.ts | 17 ++--- 11 files changed, 109 insertions(+), 99 deletions(-) diff --git a/Firebase/functions/src/common/error.ts b/Firebase/functions/src/common/error.ts index 4cd84fac..ecdc0508 100644 --- a/Firebase/functions/src/common/error.ts +++ b/Firebase/functions/src/common/error.ts @@ -1,14 +1,4 @@ -export function normalizeError(error: unknown): Record { - const normalized = error as { - code?: unknown; - details?: unknown; - message?: unknown; - stack?: unknown; - }; - return { - code: normalized?.code ?? null, - details: normalized?.details ?? null, - message: normalized?.message ?? String(error), - stack: normalized?.stack ?? null - }; +export function toError(error: unknown): Error { + if (error instanceof Error) { return error; } + return new Error(String(error)); } diff --git a/Firebase/functions/src/fcm/notification.ts b/Firebase/functions/src/fcm/notification.ts index 99232ead..5afb24da 100644 --- a/Firebase/functions/src/fcm/notification.ts +++ b/Firebase/functions/src/fcm/notification.ts @@ -2,7 +2,7 @@ import { onTaskDispatched } from "firebase-functions/v2/tasks"; import * as admin from "firebase-admin"; import * as logger from "firebase-functions/logger"; import { formatDateKey, toDate } from "../common/date"; -import { normalizeError } from "../common/error"; +import { toError } from "../common/error"; import { FirestorePath } from "../common/firestorePath"; import { resolveTimeZone } from "./shared"; @@ -131,9 +131,8 @@ export const sendPushNotification = onTaskDispatched({ } } catch (error) { - logger.error("알림 발송 중 오류 발생", { - payload: req.data, - error: normalizeError(error) + logger.error("알림 발송 중 오류 발생", toError(error), { + payload: req.data }); } } diff --git a/Firebase/functions/src/fcm/schedule.ts b/Firebase/functions/src/fcm/schedule.ts index 54f45c50..4bdd52bf 100644 --- a/Firebase/functions/src/fcm/schedule.ts +++ b/Firebase/functions/src/fcm/schedule.ts @@ -3,7 +3,7 @@ import { getFunctions } from "firebase-admin/functions"; import * as admin from "firebase-admin"; import * as logger from "firebase-functions/logger"; import { addDays, getZonedParts, zonedDateTimeToUTC } from "../common/date"; -import { normalizeError } from "../common/error"; +import { toError } from "../common/error"; import { FirestorePath } from "../common/firestorePath"; import { resolveTimeZone } from "./shared"; @@ -27,9 +27,8 @@ export const scheduleTodoReminder = onSchedule({ try { usersSnapshot = await admin.firestore().collection("users").get(); } catch (error) { - logger.error("users 조회 실패", { - at: "collection(users).get()", - ...normalizeError(error) + logger.error("users 조회 실패", toError(error), { + at: "collection(users).get()" }); return; } @@ -42,10 +41,9 @@ export const scheduleTodoReminder = onSchedule({ .doc(FirestorePath.userData(userId, FirestorePath.UserDataDocument.settings)) .get(); } catch (error) { - logger.error("settings 조회 실패", { + logger.error("settings 조회 실패", toError(error), { userId, - at: "users/{uid}/userData/settings", - ...normalizeError(error) + at: "users/{uid}/userData/settings" }); continue; } @@ -93,13 +91,12 @@ export const scheduleTodoReminder = onSchedule({ .where("dueDate", "<", admin.firestore.Timestamp.fromDate(endUTC)) .get(); } catch (error) { - logger.error("todoLists 조회 실패", { + logger.error("todoLists 조회 실패", toError(error), { userId, at: "todoLists.where(dueDate>=start).where(dueDate Date: Thu, 2 Apr 2026 13:05:41 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat:=20todoLists=20=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=EC=9D=B8=EB=8D=B1=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Firebase/firestore.index.json | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Firebase/firestore.index.json b/Firebase/firestore.index.json index bc9e032e..b90dc1cb 100644 --- a/Firebase/firestore.index.json +++ b/Firebase/firestore.index.json @@ -573,5 +573,20 @@ ] } ], - "fieldOverrides": [] + "fieldOverrides": [ + { + "collectionGroup": "todoLists", + "fieldPath": "isDeleted", + "indexes": [ + { + "order": "ASCENDING", + "queryScope": "COLLECTION_GROUP" + }, + { + "order": "DESCENDING", + "queryScope": "COLLECTION_GROUP" + } + ] + } + ] } From 0fbdacd2eba3b19a31aa74df0a6b4a052e84f522 Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 2 Apr 2026 13:05:54 +0900 Subject: [PATCH 03/10] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=20=EC=BF=BC=EB=A6=AC=20=EB=B0=8F=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EC=85=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Firebase/functions/src/todo/cleanup.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/Firebase/functions/src/todo/cleanup.ts b/Firebase/functions/src/todo/cleanup.ts index 3cf3aa45..218e56ad 100644 --- a/Firebase/functions/src/todo/cleanup.ts +++ b/Firebase/functions/src/todo/cleanup.ts @@ -81,21 +81,12 @@ export const cleanupSoftDeletedTodos = onSchedule({ }, async () => { try { - let lastDocument: - FirebaseFirestore.QueryDocumentSnapshot | undefined; - while (true) { - let query = admin.firestore() + const snapshot = await admin.firestore() .collectionGroup("todoLists") .where("isDeleted", "==", true) - .orderBy(admin.firestore.FieldPath.documentId()) - .limit(QUERY_BATCH_SIZE); - - if (lastDocument) { - query = query.startAfter(lastDocument); - } - - const snapshot = await query.get(); + .limit(QUERY_BATCH_SIZE) + .get(); if (snapshot.empty) { return; } const batch = admin.firestore().batch(); @@ -105,7 +96,6 @@ export const cleanupSoftDeletedTodos = onSchedule({ await batch.commit(); if (snapshot.size < QUERY_BATCH_SIZE) { return; } - lastDocument = snapshot.docs[snapshot.docs.length - 1]; } } catch (error) { logger.error( @@ -114,7 +104,6 @@ export const cleanupSoftDeletedTodos = onSchedule({ { collectionGroup: "todoLists", filter: "isDeleted == true", - orderBy: "__name__", queryBatchSize: QUERY_BATCH_SIZE } ); From e3c9ef41717412ddf92cb302d341b304a46811c5 Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 2 Apr 2026 13:16:59 +0900 Subject: [PATCH 04/10] =?UTF-8?q?chore:=20=EC=9D=B8=EB=8D=B1=EC=8A=A4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Firebase/firestore.index.json | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Firebase/firestore.index.json b/Firebase/firestore.index.json index b90dc1cb..e486d1b8 100644 --- a/Firebase/firestore.index.json +++ b/Firebase/firestore.index.json @@ -587,6 +587,34 @@ "queryScope": "COLLECTION_GROUP" } ] + }, + { + "collectionGroup": "notifications", + "fieldPath": "isDeleted", + "indexes": [ + { + "order": "ASCENDING", + "queryScope": "COLLECTION_GROUP" + }, + { + "order": "DESCENDING", + "queryScope": "COLLECTION_GROUP" + } + ] + }, + { + "collectionGroup": "webPages", + "fieldPath": "isDeleted", + "indexes": [ + { + "order": "ASCENDING", + "queryScope": "COLLECTION_GROUP" + }, + { + "order": "DESCENDING", + "queryScope": "COLLECTION_GROUP" + } + ] } ] } From a19dd73859cfed716b2c5a4c6f926a7a6cbca841 Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 2 Apr 2026 13:17:51 +0900 Subject: [PATCH 05/10] =?UTF-8?q?file:=20=EC=A7=80=EC=9B=8C=EC=A7=80?= =?UTF-8?q?=EB=8A=94=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EA=B8=B0=EC=A4=80?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=ED=8C=8C=EC=9D=BC=20=EC=9C=84=EC=B9=98=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Firebase/functions/src/index.ts | 8 +- .../functions/src/notification/cleanup.ts | 186 ++++++++++++++++++ Firebase/functions/src/todo/cleanup.ts | 185 ----------------- 3 files changed, 190 insertions(+), 189 deletions(-) diff --git a/Firebase/functions/src/index.ts b/Firebase/functions/src/index.ts index 9c315f5d..3d863200 100644 --- a/Firebase/functions/src/index.ts +++ b/Firebase/functions/src/index.ts @@ -33,10 +33,7 @@ import { } from "./fcm/schedule"; import { - removeTodoNotificationDocuments, - removeCompletedTodoNotificationRecords, - cleanupSoftDeletedTodos, - cleanupUnusedTodoNotificationRecords + cleanupSoftDeletedTodos } from "./todo/cleanup"; import { @@ -61,6 +58,9 @@ import { } from "./notification/deletion"; import { + removeTodoNotificationDocuments, + removeCompletedTodoNotificationRecords, + cleanupUnusedTodoNotificationRecords, cleanupSoftDeletedNotifications } from "./notification/cleanup"; diff --git a/Firebase/functions/src/notification/cleanup.ts b/Firebase/functions/src/notification/cleanup.ts index 6b2ef422..116dfdaf 100644 --- a/Firebase/functions/src/notification/cleanup.ts +++ b/Firebase/functions/src/notification/cleanup.ts @@ -1,10 +1,77 @@ +import { onDocumentDeleted, onDocumentUpdated } from "firebase-functions/v2/firestore"; import { onSchedule } from "firebase-functions/v2/scheduler"; import * as admin from "firebase-admin"; import * as logger from "firebase-functions/logger"; +import { toDate } from "../common/date"; import { toError } from "../common/error"; +import { FirestorePath } from "../common/firestorePath"; const LOCATION = "asia-northeast3"; const CLEANUP_BATCH_SIZE = 200; +const DELETE_BATCH_SIZE = 200; +const QUERY_BATCH_SIZE = 100; + +// Todo 삭제 시 연결된 알림 문서와 발송 기록 문서의 동시 제거 +export const removeTodoNotificationDocuments = onDocumentDeleted({ + maxInstances: 1, + document: "users/{userId}/todoLists/{todoId}", + region: LOCATION + }, + async (event) => { + const userId = event.params.userId; + const todoId = event.params.todoId; + + try { + await deleteByTodoId(userId, "notificationDispatches", todoId); + await deleteByTodoId(userId, "notifications", todoId); + } catch (error) { + logger.error( + "todo 삭제 후 notification 문서 정리 실패", + toError(error), + { + userId, + todoId, + collections: ["notificationDispatches", "notifications"] + } + ); + } + } +); + +// 지난 마감일 Todo 완료 시 재발송 방지 기록 정리 +export const removeCompletedTodoNotificationRecords = onDocumentUpdated({ + maxInstances: 1, + document: "users/{userId}/todoLists/{todoId}", + region: LOCATION + }, + async (event) => { + const beforeData = event.data?.before.data(); + const afterData = event.data?.after.data(); + const userId = event.params.userId; + const todoId = event.params.todoId; + + if (!beforeData || !afterData) { return; } + if (beforeData.isCompleted === true || afterData.isCompleted !== true) { return; } + + const dueDate = toDate(afterData.dueDate); + + if (!dueDate || Date.now() <= dueDate.getTime()) { return; } + + try { + await deleteByTodoId(userId, "notificationDispatches", todoId); + } catch (error) { + logger.error( + "완료된 todo의 notification record 정리 실패", + toError(error), + { + userId, + todoId, + collection: "notificationDispatches" + } + ); + } + } +); export const cleanupSoftDeletedNotifications = onSchedule({ maxInstances: 1, @@ -44,3 +111,122 @@ export const cleanupSoftDeletedNotifications = onSchedule({ } } ); + +// 사용되지 않는 알림 발송 기록의 주기적 정리 +export const cleanupUnusedTodoNotificationRecords = onSchedule({ + maxInstances: 1, + region: LOCATION, + schedule: "0 * * * *", + timeZone: "UTC" + }, + async () => { + try { + await cleanupDispatchesByTodoQuery((lastDocument) => { + let query = admin.firestore() + .collectionGroup("todoLists") + .where("isCompleted", "==", true) + .where("dueDate", "<", admin.firestore.Timestamp.now()) + .orderBy("dueDate") + .limit(QUERY_BATCH_SIZE); + + if (lastDocument) { + query = query.startAfter(lastDocument); + } + + return query; + }); + } catch (error) { + logger.error( + "지난 마감일의 완료된 todo notification record 정리 실패", + toError(error), + { + collectionGroup: "todoLists", + filter: "isCompleted == true && dueDate < now", + orderBy: "dueDate", + queryBatchSize: QUERY_BATCH_SIZE + } + ); + } + + try { + await cleanupDispatchesByTodoQuery((lastDocument) => { + let query = admin.firestore() + .collectionGroup("todoLists") + .where("dueDate", "==", null) + .orderBy(admin.firestore.FieldPath.documentId()) + .limit(QUERY_BATCH_SIZE); + + if (lastDocument) { + query = query.startAfter(lastDocument); + } + + return query; + }); + } catch (error) { + logger.error( + "마감일이 없는 todo notification record 정리 실패", + toError(error), + { + collectionGroup: "todoLists", + filter: "dueDate == null", + orderBy: "__name__", + queryBatchSize: QUERY_BATCH_SIZE + } + ); + } + } +); + +// Todo 조회 쿼리를 순회하며 연결된 알림 발송 기록을 정리 +async function cleanupDispatchesByTodoQuery( + makeQuery: ( + lastDocument?: + FirebaseFirestore.QueryDocumentSnapshot + ) => FirebaseFirestore.Query +): Promise { + let lastDocument: + FirebaseFirestore.QueryDocumentSnapshot | undefined; + + while (true) { + const snapshot = await makeQuery(lastDocument).get(); + if (snapshot.empty) { return; } + + for (const todoDoc of snapshot.docs) { + const userId = todoDoc.ref.parent.parent?.id; + if (!userId) { continue; } + + await deleteByTodoId(userId, "notificationDispatches", todoDoc.id); + } + + if (snapshot.size < QUERY_BATCH_SIZE) { return; } + lastDocument = snapshot.docs[snapshot.docs.length - 1]; + } +} + +// 특정 Todo 연결 문서의 배치 단위 전체 삭제 +async function deleteByTodoId( + userId: string, + collectionName: "notificationDispatches" | "notifications", + todoId: string +): Promise { + while (true) { + const collectionPath = collectionName === "notificationDispatches" ? + FirestorePath.notificationDispatches(userId) : + FirestorePath.notifications(userId); + const snapshot = await admin.firestore() + .collection(collectionPath) + .where("todoId", "==", todoId) + .limit(DELETE_BATCH_SIZE) + .get(); + + if (snapshot.empty) { return; } + + const batch = admin.firestore().batch(); + snapshot.docs.forEach((document) => { + batch.delete(document.ref); + }); + await batch.commit(); + + if (snapshot.size < DELETE_BATCH_SIZE) { return; } + } +} diff --git a/Firebase/functions/src/todo/cleanup.ts b/Firebase/functions/src/todo/cleanup.ts index 218e56ad..ab8072ad 100644 --- a/Firebase/functions/src/todo/cleanup.ts +++ b/Firebase/functions/src/todo/cleanup.ts @@ -1,77 +1,11 @@ -import { onDocumentDeleted, onDocumentUpdated } from "firebase-functions/v2/firestore"; import { onSchedule } from "firebase-functions/v2/scheduler"; import * as admin from "firebase-admin"; import * as logger from "firebase-functions/logger"; -import { toDate } from "../common/date"; import { toError } from "../common/error"; -import { FirestorePath } from "../common/firestorePath"; const LOCATION = "asia-northeast3"; -const DELETE_BATCH_SIZE = 200; const QUERY_BATCH_SIZE = 100; -// Todo 삭제 시 연결된 알림 문서와 발송 기록 문서의 동시 제거 -export const removeTodoNotificationDocuments = onDocumentDeleted({ - maxInstances: 1, - document: "users/{userId}/todoLists/{todoId}", - region: LOCATION - }, - async (event) => { - const userId = event.params.userId; - const todoId = event.params.todoId; - - try { - await deleteByTodoId(userId, "notificationDispatches", todoId); - await deleteByTodoId(userId, "notifications", todoId); - } catch (error) { - logger.error( - "todo 삭제 후 notification 문서 정리 실패", - toError(error), - { - userId, - todoId, - collections: ["notificationDispatches", "notifications"] - } - ); - } - } -); - -// 지난 마감일 Todo 완료 시 재발송 방지 기록 정리 -export const removeCompletedTodoNotificationRecords = onDocumentUpdated({ - maxInstances: 1, - document: "users/{userId}/todoLists/{todoId}", - region: LOCATION - }, - async (event) => { - const beforeData = event.data?.before.data(); - const afterData = event.data?.after.data(); - const userId = event.params.userId; - const todoId = event.params.todoId; - - if (!beforeData || !afterData) { return; } - if (beforeData.isCompleted === true || afterData.isCompleted !== true) { return; } - - const dueDate = toDate(afterData.dueDate); - - if (!dueDate || Date.now() <= dueDate.getTime()) { return; } - - try { - await deleteByTodoId(userId, "notificationDispatches", todoId); - } catch (error) { - logger.error( - "완료된 todo의 notification record 정리 실패", - toError(error), - { - userId, - todoId, - collection: "notificationDispatches" - } - ); - } - } -); - // soft delete Todo 문서의 실제 삭제 export const cleanupSoftDeletedTodos = onSchedule({ maxInstances: 1, @@ -110,122 +44,3 @@ export const cleanupSoftDeletedTodos = onSchedule({ } } ); - -// 사용되지 않는 알림 발송 기록의 주기적 정리 -export const cleanupUnusedTodoNotificationRecords = onSchedule({ - maxInstances: 1, - region: LOCATION, - schedule: "0 * * * *", - timeZone: "UTC" - }, - async () => { - try { - await cleanupDispatchesByTodoQuery((lastDocument) => { - let query = admin.firestore() - .collectionGroup("todoLists") - .where("isCompleted", "==", true) - .where("dueDate", "<", admin.firestore.Timestamp.now()) - .orderBy("dueDate") - .limit(QUERY_BATCH_SIZE); - - if (lastDocument) { - query = query.startAfter(lastDocument); - } - - return query; - }); - } catch (error) { - logger.error( - "지난 마감일의 완료된 todo notification record 정리 실패", - toError(error), - { - collectionGroup: "todoLists", - filter: "isCompleted == true && dueDate < now", - orderBy: "dueDate", - queryBatchSize: QUERY_BATCH_SIZE - } - ); - } - - try { - await cleanupDispatchesByTodoQuery((lastDocument) => { - let query = admin.firestore() - .collectionGroup("todoLists") - .where("dueDate", "==", null) - .orderBy(admin.firestore.FieldPath.documentId()) - .limit(QUERY_BATCH_SIZE); - - if (lastDocument) { - query = query.startAfter(lastDocument); - } - - return query; - }); - } catch (error) { - logger.error( - "마감일이 없는 todo notification record 정리 실패", - toError(error), - { - collectionGroup: "todoLists", - filter: "dueDate == null", - orderBy: "__name__", - queryBatchSize: QUERY_BATCH_SIZE - } - ); - } - } -); - -// Todo 조회 쿼리를 순회하며 연결된 알림 발송 기록을 정리 -async function cleanupDispatchesByTodoQuery( - makeQuery: ( - lastDocument?: - FirebaseFirestore.QueryDocumentSnapshot - ) => FirebaseFirestore.Query -): Promise { - let lastDocument: - FirebaseFirestore.QueryDocumentSnapshot | undefined; - - while (true) { - const snapshot = await makeQuery(lastDocument).get(); - if (snapshot.empty) { return; } - - for (const todoDoc of snapshot.docs) { - const userId = todoDoc.ref.parent.parent?.id; - if (!userId) { continue; } - - await deleteByTodoId(userId, "notificationDispatches", todoDoc.id); - } - - if (snapshot.size < QUERY_BATCH_SIZE) { return; } - lastDocument = snapshot.docs[snapshot.docs.length - 1]; - } -} - -// 특정 Todo 연결 문서의 배치 단위 전체 삭제 -async function deleteByTodoId( - userId: string, - collectionName: "notificationDispatches" | "notifications", - todoId: string -): Promise { - while (true) { - const collectionPath = collectionName === "notificationDispatches" ? - FirestorePath.notificationDispatches(userId) : - FirestorePath.notifications(userId); - const snapshot = await admin.firestore() - .collection(collectionPath) - .where("todoId", "==", todoId) - .limit(DELETE_BATCH_SIZE) - .get(); - - if (snapshot.empty) { return; } - - const batch = admin.firestore().batch(); - snapshot.docs.forEach((document) => { - batch.delete(document.ref); - }); - await batch.commit(); - - if (snapshot.size < DELETE_BATCH_SIZE) { return; } - } -} From 6db4fa572a4d420c6d4acb25f553f2a2dc0b45be Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 3 Apr 2026 00:05:32 +0900 Subject: [PATCH 06/10] =?UTF-8?q?feat:=20=ED=91=B8=EC=8B=9C=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20soft=20delete=20=EB=B0=8F=20Firestore=20=EC=9D=B8?= =?UTF-8?q?=EB=8D=B1=EC=8A=A4=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Firebase/firestore.index.json | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Firebase/firestore.index.json b/Firebase/firestore.index.json index e486d1b8..79aefd54 100644 --- a/Firebase/firestore.index.json +++ b/Firebase/firestore.index.json @@ -578,6 +578,14 @@ "collectionGroup": "todoLists", "fieldPath": "isDeleted", "indexes": [ + { + "order": "ASCENDING", + "queryScope": "COLLECTION" + }, + { + "order": "DESCENDING", + "queryScope": "COLLECTION" + }, { "order": "ASCENDING", "queryScope": "COLLECTION_GROUP" @@ -592,6 +600,14 @@ "collectionGroup": "notifications", "fieldPath": "isDeleted", "indexes": [ + { + "order": "ASCENDING", + "queryScope": "COLLECTION" + }, + { + "order": "DESCENDING", + "queryScope": "COLLECTION" + }, { "order": "ASCENDING", "queryScope": "COLLECTION_GROUP" @@ -606,6 +622,14 @@ "collectionGroup": "webPages", "fieldPath": "isDeleted", "indexes": [ + { + "order": "ASCENDING", + "queryScope": "COLLECTION" + }, + { + "order": "DESCENDING", + "queryScope": "COLLECTION" + }, { "order": "ASCENDING", "queryScope": "COLLECTION_GROUP" From 7c102586caca5c5f0c1e2b7522acc70af58b8dc2 Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 3 Apr 2026 00:51:45 +0900 Subject: [PATCH 07/10] =?UTF-8?q?feat:=20documentID=20=EA=B8=B0=EC=A4=80?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=98=A4=EB=A6=84=EC=B0=A8=EC=88=9C=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=ED=95=98=EC=97=AC=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Firebase/firestore.index.json | 4 ++++ Firebase/functions/src/notification/cleanup.ts | 17 ++++++++++++++--- Firebase/functions/src/todo/cleanup.ts | 14 ++++++++++++-- Firebase/functions/src/webPage/cleanup.ts | 14 ++++++++++++-- 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/Firebase/firestore.index.json b/Firebase/firestore.index.json index 79aefd54..5b8814d9 100644 --- a/Firebase/firestore.index.json +++ b/Firebase/firestore.index.json @@ -11,6 +11,10 @@ { "fieldPath": "dueDate", "order": "ASCENDING" + }, + { + "fieldPath": "__name__", + "order": "ASCENDING" } ] }, diff --git a/Firebase/functions/src/notification/cleanup.ts b/Firebase/functions/src/notification/cleanup.ts index 116dfdaf..d624d32e 100644 --- a/Firebase/functions/src/notification/cleanup.ts +++ b/Firebase/functions/src/notification/cleanup.ts @@ -81,12 +81,20 @@ export const cleanupSoftDeletedNotifications = onSchedule({ }, async () => { try { + let lastDocument: + FirebaseFirestore.QueryDocumentSnapshot | undefined; + while (true) { - const snapshot = await admin.firestore() + let query = admin.firestore() .collectionGroup("notifications") .where("isDeleted", "==", true) + .orderBy(admin.firestore.FieldPath.documentId()) .limit(CLEANUP_BATCH_SIZE) - .get(); + if (lastDocument) { + query = query.startAfter(lastDocument); + } + + const snapshot = await query.get(); if (snapshot.empty) { return; } @@ -97,6 +105,7 @@ export const cleanupSoftDeletedNotifications = onSchedule({ await batch.commit(); if (snapshot.size < CLEANUP_BATCH_SIZE) { return; } + lastDocument = snapshot.docs[snapshot.docs.length - 1]; } } catch (error) { logger.error( @@ -105,6 +114,7 @@ export const cleanupSoftDeletedNotifications = onSchedule({ { collectionGroup: "notifications", filter: "isDeleted == true", + orderBy: "documentId", cleanupBatchSize: CLEANUP_BATCH_SIZE } ); @@ -127,6 +137,7 @@ export const cleanupUnusedTodoNotificationRecords = onSchedule({ .where("isCompleted", "==", true) .where("dueDate", "<", admin.firestore.Timestamp.now()) .orderBy("dueDate") + .orderBy(admin.firestore.FieldPath.documentId()) .limit(QUERY_BATCH_SIZE); if (lastDocument) { @@ -142,7 +153,7 @@ export const cleanupUnusedTodoNotificationRecords = onSchedule({ { collectionGroup: "todoLists", filter: "isCompleted == true && dueDate < now", - orderBy: "dueDate", + orderBy: ["dueDate", "documentId"], queryBatchSize: QUERY_BATCH_SIZE } ); diff --git a/Firebase/functions/src/todo/cleanup.ts b/Firebase/functions/src/todo/cleanup.ts index ab8072ad..4525c5b4 100644 --- a/Firebase/functions/src/todo/cleanup.ts +++ b/Firebase/functions/src/todo/cleanup.ts @@ -15,12 +15,20 @@ export const cleanupSoftDeletedTodos = onSchedule({ }, async () => { try { + let lastDocument: + FirebaseFirestore.QueryDocumentSnapshot | undefined; + while (true) { - const snapshot = await admin.firestore() + let query = admin.firestore() .collectionGroup("todoLists") .where("isDeleted", "==", true) + .orderBy(admin.firestore.FieldPath.documentId()) .limit(QUERY_BATCH_SIZE) - .get(); + if (lastDocument) { + query = query.startAfter(lastDocument); + } + + const snapshot = await query.get(); if (snapshot.empty) { return; } const batch = admin.firestore().batch(); @@ -30,6 +38,7 @@ export const cleanupSoftDeletedTodos = onSchedule({ await batch.commit(); if (snapshot.size < QUERY_BATCH_SIZE) { return; } + lastDocument = snapshot.docs[snapshot.docs.length - 1]; } } catch (error) { logger.error( @@ -38,6 +47,7 @@ export const cleanupSoftDeletedTodos = onSchedule({ { collectionGroup: "todoLists", filter: "isDeleted == true", + orderBy: "documentId", queryBatchSize: QUERY_BATCH_SIZE } ); diff --git a/Firebase/functions/src/webPage/cleanup.ts b/Firebase/functions/src/webPage/cleanup.ts index 36e357eb..5eaec4b9 100644 --- a/Firebase/functions/src/webPage/cleanup.ts +++ b/Firebase/functions/src/webPage/cleanup.ts @@ -14,12 +14,20 @@ export const cleanupSoftDeletedWebPages = onSchedule({ }, async () => { try { + let lastDocument: + FirebaseFirestore.QueryDocumentSnapshot | undefined; + while (true) { - const snapshot = await admin.firestore() + let query = admin.firestore() .collectionGroup("webPages") .where("isDeleted", "==", true) + .orderBy(admin.firestore.FieldPath.documentId()) .limit(CLEANUP_BATCH_SIZE) - .get(); + if (lastDocument) { + query = query.startAfter(lastDocument); + } + + const snapshot = await query.get(); if (snapshot.empty) { return; } @@ -30,11 +38,13 @@ export const cleanupSoftDeletedWebPages = onSchedule({ await batch.commit(); if (snapshot.size < CLEANUP_BATCH_SIZE) { return; } + lastDocument = snapshot.docs[snapshot.docs.length - 1]; } } catch (error) { logger.error("soft delete WebPage cleanup 실패", toError(error), { collectionGroup: "webPages", filter: "isDeleted == true", + orderBy: "documentId", cleanupBatchSize: CLEANUP_BATCH_SIZE }); } From faba21bb7fdb7027ae2160653e4502831495417d Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 3 Apr 2026 09:43:50 +0900 Subject: [PATCH 08/10] =?UTF-8?q?chore:=20=EC=A7=81=EA=B4=80=EC=A0=81?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=95=8A=EC=9D=80=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Firebase/functions/src/index.ts | 4 ++-- Firebase/functions/src/notification/cleanup.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Firebase/functions/src/index.ts b/Firebase/functions/src/index.ts index 3d863200..87b8691b 100644 --- a/Firebase/functions/src/index.ts +++ b/Firebase/functions/src/index.ts @@ -60,7 +60,7 @@ import { import { removeTodoNotificationDocuments, removeCompletedTodoNotificationRecords, - cleanupUnusedTodoNotificationRecords, + cleanupNotificationDispatches, cleanupSoftDeletedNotifications } from "./notification/cleanup"; @@ -114,7 +114,7 @@ export { removeTodoNotificationDocuments, removeCompletedTodoNotificationRecords, cleanupSoftDeletedTodos, - cleanupUnusedTodoNotificationRecords, + cleanupNotificationDispatches, syncTodoNotificationCategory, requestMoveRemovedCategoryTodosToEtc, completeMoveRemovedCategoryTodosToEtc, diff --git a/Firebase/functions/src/notification/cleanup.ts b/Firebase/functions/src/notification/cleanup.ts index d624d32e..81df0242 100644 --- a/Firebase/functions/src/notification/cleanup.ts +++ b/Firebase/functions/src/notification/cleanup.ts @@ -122,8 +122,8 @@ export const cleanupSoftDeletedNotifications = onSchedule({ } ); -// 사용되지 않는 알림 발송 기록의 주기적 정리 -export const cleanupUnusedTodoNotificationRecords = onSchedule({ +// 더 이상 필요하지 않은 알림 발송 기록 정리 +export const cleanupNotificationDispatches = onSchedule({ maxInstances: 1, region: LOCATION, schedule: "0 * * * *", From 06ecea0182a63afb918773ba03e00437da2015ed Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 3 Apr 2026 10:10:17 +0900 Subject: [PATCH 09/10] =?UTF-8?q?feat:=20=EB=8B=A8=EC=9D=BC=20=EC=9D=B8?= =?UTF-8?q?=EB=8D=B1=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Firebase/firestore.index.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Firebase/firestore.index.json b/Firebase/firestore.index.json index 5b8814d9..eeda2a59 100644 --- a/Firebase/firestore.index.json +++ b/Firebase/firestore.index.json @@ -578,6 +578,28 @@ } ], "fieldOverrides": [ + { + "collectionGroup": "todoLists", + "fieldPath": "dueDate", + "indexes": [ + { + "order": "ASCENDING", + "queryScope": "COLLECTION" + }, + { + "order": "DESCENDING", + "queryScope": "COLLECTION" + }, + { + "order": "ASCENDING", + "queryScope": "COLLECTION_GROUP" + }, + { + "order": "DESCENDING", + "queryScope": "COLLECTION_GROUP" + } + ] + }, { "collectionGroup": "todoLists", "fieldPath": "isDeleted", From 2afda0bec87c5a2f08d251cfa4fc28e161f6a53f Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 3 Apr 2026 10:27:43 +0900 Subject: [PATCH 10/10] =?UTF-8?q?fix:=20=EC=9E=98=EB=AA=BB=EB=90=9C=20?= =?UTF-8?q?=EC=8A=A4=EC=BC=80=EC=A4=84=EB=A7=81=20=ED=83=80=EC=9D=B4?= =?UTF-8?q?=EB=A8=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Firebase/functions/src/notification/cleanup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firebase/functions/src/notification/cleanup.ts b/Firebase/functions/src/notification/cleanup.ts index 81df0242..883cab36 100644 --- a/Firebase/functions/src/notification/cleanup.ts +++ b/Firebase/functions/src/notification/cleanup.ts @@ -126,7 +126,7 @@ export const cleanupSoftDeletedNotifications = onSchedule({ export const cleanupNotificationDispatches = onSchedule({ maxInstances: 1, region: LOCATION, - schedule: "0 * * * *", + schedule: "0 0 * * *", timeZone: "UTC" }, async () => {