feat: 알림 신뢰성·내용 보강 (SSE 재동기화·웹푸시·전직원 일정·파일첨부)
[신뢰성] - SSE 재연결 시 fetchInitial 로 재동기화: 끊긴 사이 놓친 알림 복구 + access 토큰 자동 refresh 로 만료 토큰 재연결 무한루프 해소 - 서비스워커 pushsubscriptionchange 처리로 구독 회전 시 자동 재등록 - 읽은 알림 90일 retention 정리 배치(@Cron) - 웹푸시 다기기 병렬 발송(allSettled) + TTL/urgency 지정 - 알림 문구 SSOT 상호참조 주석 + web-push 커버리지 테스트 추가 [내용] - 전 직원(allHands) 일정도 전 사용자에게 알림(생성/수정/삭제) - task.file_attached 신규 타입: 파일 첨부 시 지시자·담당자 알림 - task.started 재개(수정요청 후) 문구 '재개/시작' 구분 - task.due_changed 푸시에 날짜/해제 상세 반영 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,49 @@ self.addEventListener('push', (event) => {
|
||||
)
|
||||
})
|
||||
|
||||
// 브라우저가 푸시 구독을 자체 회전(만료·교체)시키면 발생 — 새 구독으로 재등록해
|
||||
// 사용자가 앱을 다시 열지 않아도 푸시가 끊기지 않게 한다. best-effort(실패 시 앱 재진입의
|
||||
// enablePush(true) 가 최신화). 백엔드 경로는 기본 프록시('/api') 기준.
|
||||
self.addEventListener('pushsubscriptionchange', (event) => {
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
try {
|
||||
// 브라우저가 새 구독을 넘겨줬으면 그대로, 아니면 이전 구독의 VAPID 키로 재구독
|
||||
let sub = event.newSubscription || null
|
||||
if (!sub) {
|
||||
const opts =
|
||||
event.oldSubscription && event.oldSubscription.options
|
||||
? event.oldSubscription.options
|
||||
: null
|
||||
const appServerKey = opts && opts.applicationServerKey
|
||||
// 키가 없으면 재구독 불가 — 앱 재진입 때 복구되도록 조용히 종료
|
||||
if (!appServerKey) return
|
||||
sub = await self.registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: appServerKey,
|
||||
})
|
||||
}
|
||||
if (!sub) return
|
||||
const json = sub.toJSON()
|
||||
await fetch('/api/notifications/push/subscribe', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
endpoint: sub.endpoint,
|
||||
keys: {
|
||||
p256dh: (json.keys && json.keys.p256dh) || '',
|
||||
auth: (json.keys && json.keys.auth) || '',
|
||||
},
|
||||
}),
|
||||
})
|
||||
} catch (e) {
|
||||
/* 재구독/재등록 실패 무시 — 앱 재진입 시 최신화 */
|
||||
}
|
||||
})(),
|
||||
)
|
||||
})
|
||||
|
||||
self.addEventListener('notificationclick', (event) => {
|
||||
event.notification.close()
|
||||
const url = (event.notification.data && event.notification.data.url) || '/'
|
||||
|
||||
Reference in New Issue
Block a user