Skip to content

Commit 09326d1

Browse files
committed
refactor: 푸시알람 리스트 제거 undo 시 별도의 Task 문서를 생성하지 않도록 개선
1 parent 4b82454 commit 09326d1

3 files changed

Lines changed: 81 additions & 59 deletions

File tree

Firebase/functions/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ import {
6060
completePushNotificationDeletion
6161
} from "./notification/deletion";
6262

63+
import {
64+
cleanupSoftDeletedNotifications
65+
} from "./notification/cleanup";
66+
6367
import {
6468
requestWebPageDeletion,
6569
undoWebPageDeletion,
@@ -120,6 +124,7 @@ export {
120124
requestPushNotificationDeletion,
121125
undoPushNotificationDeletion,
122126
completePushNotificationDeletion,
127+
cleanupSoftDeletedNotifications,
123128
requestWebPageDeletion,
124129
undoWebPageDeletion,
125130
completeWebPageDeletion,
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { onSchedule } from "firebase-functions/v2/scheduler";
2+
import * as admin from "firebase-admin";
3+
import * as logger from "firebase-functions/logger";
4+
import { normalizeError } from "../common/error";
5+
6+
const LOCATION = "asia-northeast3";
7+
const CLEANUP_BATCH_SIZE = 200;
8+
9+
export const cleanupSoftDeletedNotifications = onSchedule({
10+
maxInstances: 1,
11+
region: LOCATION,
12+
schedule: "0 0 * * *",
13+
timeZone: "UTC"
14+
},
15+
async () => {
16+
try {
17+
while (true) {
18+
const snapshot = await admin.firestore()
19+
.collectionGroup("notifications")
20+
.where("isDeleted", "==", true)
21+
.limit(CLEANUP_BATCH_SIZE)
22+
.get();
23+
24+
if (snapshot.empty) { return; }
25+
26+
const batch = admin.firestore().batch();
27+
snapshot.docs.forEach((document) => {
28+
batch.delete(document.ref);
29+
});
30+
await batch.commit();
31+
32+
if (snapshot.size < CLEANUP_BATCH_SIZE) { return; }
33+
}
34+
} catch (error) {
35+
logger.error("soft delete Notification cleanup 실패", {
36+
error: normalizeError(error)
37+
});
38+
}
39+
}
40+
);

Firebase/functions/src/notification/deletion.ts

Lines changed: 36 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@ import { FirestorePath } from "../common/firestorePath";
99
const LOCATION = "asia-northeast3";
1010
const DELETE_DELAY_SECONDS = 5;
1111

12-
type NotificationDeletionTaskData = {
12+
type NotificationDeletionPayload = {
1313
userId: string;
1414
notificationId: string;
15-
createdAt?: FirebaseFirestore.Timestamp | Date | null;
1615
};
1716

1817
export const requestPushNotificationDeletion = onCall({
@@ -37,45 +36,27 @@ export const requestPushNotificationDeletion = onCall({
3736
const notificationRef = admin.firestore().doc(FirestorePath.notification(userId, notificationId));
3837
const notificationSnapshot = await notificationRef.get();
3938

40-
if (!notificationSnapshot.exists) {
39+
if (!notificationSnapshot.exists || notificationSnapshot.data()?.isDeleted === true) {
4140
throw new HttpsError("not-found", "Notification을 찾을 수 없습니다.");
4241
}
4342

44-
const taskRef = admin.firestore().collection("notificationDeletionTasks").doc();
45-
const taskData = {
46-
userId,
47-
notificationId,
48-
createdAt: admin.firestore.FieldValue.serverTimestamp()
49-
};
50-
5143
try {
52-
await taskRef.set(taskData);
5344
await notificationRef.set({
5445
// deletingAt: 삭제 요청은 되었지만, 5초 유예 후 최종 삭제되기 전 상태를 의미한다.
55-
deletingAt: admin.firestore.FieldValue.serverTimestamp()
46+
deletingAt: admin.firestore.FieldValue.serverTimestamp(),
47+
isDeleted: false
5648
}, {merge: true});
5749

5850
const queue = getFunctions().taskQueue(
5951
`locations/${LOCATION}/functions/completePushNotificationDeletion`
6052
);
6153
await queue.enqueue(
62-
{taskId: taskRef.id},
54+
{ userId, notificationId },
6355
{scheduleDelaySeconds: DELETE_DELAY_SECONDS}
6456
);
6557
} catch (error) {
66-
try {
67-
await taskRef.delete();
68-
} catch (cleanupError) {
69-
logger.warn("notificationDeletionTasks 정리 실패", {
70-
userId,
71-
notificationId,
72-
taskId: taskRef.id,
73-
error: normalizeError(cleanupError)
74-
});
75-
}
76-
7758
const currentNotificationSnapshot = await notificationRef.get();
78-
if (currentNotificationSnapshot.exists) {
59+
if (currentNotificationSnapshot.exists && currentNotificationSnapshot.data()?.isDeleted !== true) {
7960
await notificationRef.update({
8061
deletingAt: admin.firestore.FieldValue.delete()
8162
});
@@ -111,28 +92,15 @@ export const undoPushNotificationDeletion = onCall({
11192
if (!notificationId) {
11293
throw new HttpsError("invalid-argument", "notificationId가 필요합니다.");
11394
}
114-
115-
const taskSnapshot = await admin.firestore()
116-
.collection("notificationDeletionTasks")
117-
.where("userId", "==", userId)
118-
.where("notificationId", "==", notificationId)
119-
.get();
12095
const notificationRef = admin.firestore().doc(FirestorePath.notification(userId, notificationId));
12196

12297
try {
12398
const notificationSnapshot = await notificationRef.get();
124-
if (notificationSnapshot.exists) {
99+
if (notificationSnapshot.exists && notificationSnapshot.data()?.isDeleted !== true) {
125100
await notificationRef.update({
126-
deletingAt: admin.firestore.FieldValue.delete()
127-
});
128-
}
129-
130-
if (!taskSnapshot.empty) {
131-
const batch = admin.firestore().batch();
132-
taskSnapshot.docs.forEach((document) => {
133-
batch.delete(document.ref);
101+
deletingAt: admin.firestore.FieldValue.delete(),
102+
isDeleted: false
134103
});
135-
await batch.commit();
136104
}
137105
} catch (error) {
138106
logger.error("푸시 알림 삭제 취소 실패", {
@@ -154,45 +122,54 @@ export const completePushNotificationDeletion = onTaskDispatched({
154122
rateLimits: {maxDispatchesPerSecond: 200},
155123
},
156124
async (request) => {
157-
const taskId = typeof request.data?.taskId === "string" ? request.data.taskId.trim() : "";
158-
if (!taskId) {
125+
const payload = parseDeletionPayload(request.data);
126+
if (!payload) {
159127
logger.warn("유효하지 않은 푸시 알림 삭제 payload", request.data);
160128
return;
161129
}
162130

163-
const taskRef = admin.firestore().collection("notificationDeletionTasks").doc(taskId);
164-
const taskSnapshot = await taskRef.get();
165-
if (!taskSnapshot.exists) { return; }
166-
167-
const taskData = taskSnapshot.data() as NotificationDeletionTaskData | undefined;
168-
const userId = typeof taskData?.userId === "string" ? taskData.userId : "";
169-
const notificationId = typeof taskData?.notificationId === "string" ? taskData.notificationId : "";
170-
if (!userId || !notificationId) {
171-
logger.warn("notificationDeletionTasks 문서 형식이 올바르지 않습니다.", {taskId});
172-
return;
173-
}
131+
const { userId, notificationId } = payload;
174132

175133
const notificationRef = admin.firestore().doc(FirestorePath.notification(userId, notificationId));
176134

177135
try {
178136
const notificationSnapshot = await notificationRef.get();
179137
const deletingAt = notificationSnapshot.data()?.deletingAt;
138+
const isDeleted = notificationSnapshot.data()?.isDeleted === true;
180139

181-
if (!notificationSnapshot.exists || !deletingAt) {
182-
await taskRef.delete();
140+
if (!notificationSnapshot.exists || !deletingAt || isDeleted) {
183141
return;
184142
}
185143

186-
await notificationRef.delete();
187-
await taskRef.delete();
144+
await notificationRef.set({
145+
deletingAt: admin.firestore.FieldValue.delete(),
146+
isDeleted: true
147+
}, { merge: true });
188148
} catch (error) {
189149
logger.error("푸시 알림 최종 삭제 실패", {
190150
userId,
191151
notificationId,
192-
taskId,
193152
error: normalizeError(error)
194153
});
195154
throw error;
196155
}
197156
}
198157
);
158+
159+
function parseDeletionPayload(data: unknown): NotificationDeletionPayload | null {
160+
const userId = typeof (data as NotificationDeletionPayload | undefined)?.userId === "string" ?
161+
(data as NotificationDeletionPayload).userId.trim() :
162+
"";
163+
const notificationId = typeof (data as NotificationDeletionPayload | undefined)?.notificationId === "string" ?
164+
(data as NotificationDeletionPayload).notificationId.trim() :
165+
"";
166+
167+
if (!userId || !notificationId) {
168+
return null;
169+
}
170+
171+
return {
172+
userId,
173+
notificationId
174+
};
175+
}

0 commit comments

Comments
 (0)