Files
distribution_erp/public/sw.js
T
chpark b5302c52d2
Deploy momo-erp / deploy (push) Successful in 3m34s
feat(push): PWA 웹 푸시 — 품목이 출고요청 가능해지면 구독자에게 알림
- 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_* 설정(없으면 기본키 사용).
2026-05-27 00:17:54 +09:00

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);
})
);
});