b5302c52d2
Deploy momo-erp / deploy (push) Successful in 3m34s
- lib/push.ts: web-push + VAPID(env 우선/하드코딩 폴백) + momo_push_subscriptions 자동 생성. sendPush() 는 만료(404/410) 구독 자동 정리. - API: GET /api/m/push/vapid (공개키), POST /api/m/push/subscribe (구독 저장). - sw.js: push / notificationclick 핸들러 추가 (클릭 시 /m/orders/new 열기). - components/PushOptIn: 출고요청 페이지에 '새 품목 알림 받기' 버튼. 권한 허용 시 구독 저장, 이미 허용이면 조용히 갱신. iOS<16.4 등 미지원 환경은 자동 숨김. - items/save: 품목이 '출고요청 불가 → 가능' 으로 전환되면(신규 등록 포함, KST 기준 판매기간/ACTIVE/비숨김) 구독자에게 푸시 발송. 단순 수정은 알림 안 함. 운영에서 VAPID 키 교체 원하면 .env.production 에 VAPID_* 설정(없으면 기본키 사용).
56 lines
1.8 KiB
JavaScript
56 lines
1.8 KiB
JavaScript
// 모모유통 ERP — Service Worker (PWA install criteria 충족용)
|
|
const CACHE = 'momo-erp-v1';
|
|
const PRECACHE = ['/', '/manifest.json', '/icon-192.png', '/icon-512.png'];
|
|
|
|
self.addEventListener('install', (e) => {
|
|
e.waitUntil(caches.open(CACHE).then((c) => c.addAll(PRECACHE)).catch(() => {}));
|
|
self.skipWaiting();
|
|
});
|
|
|
|
self.addEventListener('activate', (e) => {
|
|
e.waitUntil(
|
|
caches.keys().then((keys) =>
|
|
Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))
|
|
)
|
|
);
|
|
self.clients.claim();
|
|
});
|
|
|
|
// API 요청은 항상 네트워크 (캐시 안 함). 정적 자원만 캐시.
|
|
self.addEventListener('fetch', (e) => {
|
|
const url = new URL(e.request.url);
|
|
if (url.pathname.startsWith('/api/')) return;
|
|
if (e.request.method !== 'GET') return;
|
|
e.respondWith(
|
|
fetch(e.request).catch(() => caches.match(e.request))
|
|
);
|
|
});
|
|
|
|
// ===== 웹 푸시 =====
|
|
self.addEventListener('push', (e) => {
|
|
let data = {};
|
|
try { data = e.data ? e.data.json() : {}; } catch (_) { data = {}; }
|
|
const title = data.title || '모모유통';
|
|
const options = {
|
|
body: data.body || '',
|
|
icon: '/icon-192.png',
|
|
badge: '/icon-192.png',
|
|
tag: data.tag || undefined,
|
|
data: { url: data.url || '/m/orders/new' },
|
|
};
|
|
e.waitUntil(self.registration.showNotification(title, options));
|
|
});
|
|
|
|
self.addEventListener('notificationclick', (e) => {
|
|
e.notification.close();
|
|
const target = (e.notification.data && e.notification.data.url) || '/m/orders/new';
|
|
e.waitUntil(
|
|
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((cs) => {
|
|
for (const c of cs) {
|
|
if ('focus' in c) { c.navigate(target); return c.focus(); }
|
|
}
|
|
if (self.clients.openWindow) return self.clients.openWindow(target);
|
|
})
|
|
);
|
|
});
|