Files
distribution_erp/public/sw.js
T
chpark 5bd07526e4 fix(push): 알림에 본문/이미지 표시 — FCM image + sw.js image 옵션
증상: 폰에 도착한 푸시 알림이 제목만 보이고 본문/이미지 안 보임
원인:
  1) FCM android.notification 에 image 필드 미설정 → big picture 안 뜸
  2) 본문이 비어있으면 안드로이드가 알림을 dismiss
  3) 본문 HTML 안의 이미지가 푸시 발송 흐름과 분리됨

수정:
- src/lib/firebase-push.ts:
  + FcmPayload.image 추가
  + android.notification.image (big picture) + notification.image
  + notification_priority: PRIORITY_MAX, visibility: PUBLIC
  + body 빈값 대비 ' ' 폴백
- src/lib/push.ts:
  + PushPayload.image 추가
  + web-push body JSON 에 image 포함 → sw.js 에서 그대로 사용
- src/app/api/m/admin/notices/send-push/route.ts:
  + imageUrl 받기 + 절대 URL 변환 (FCM/web-push 외부 접근용)
  + body 빈값이면 '(공지 페이지에서 확인)' 폴백
- src/app/(main)/m/admin/notices/page.tsx:
  + 첨부 이미지 없으면 본문 HTML 의 첫 <img> 자동 추출
  + send-push 호출 시 imageUrl 전달
- public/sw.js v4:
  + showNotification options.image 추가 (web-push 브라우저 큰 이미지)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 01:04:12 +09:00

67 lines
2.5 KiB
JavaScript

// 모모유통 ERP — Service Worker (PWA install criteria 충족용)
const CACHE = 'momo-erp-v4';
const PRECACHE = ['/', '/manifest.json', '/icon-192.png', '/icon-512.png', '/badge-96.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))
);
});
// ===== 웹 푸시 =====
// 삼성 인터넷/Samsung Galaxy 에서 상단 배너(heads-up) 가 안 뜨던 문제 해결:
// - vibrate: 패턴 명시 (안드로이드가 high-priority 채널로 분류)
// - requireInteraction: 사용자가 직접 닫을 때까지 유지
// - renotify: 같은 tag 라도 다시 알림
// - silent: false 명시 (Samsung 일부 버전에서 기본값이 true 인 케이스 회피)
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: data.icon || '/icon-192.png', // 큰 아이콘 = 모모 로고(초록 M)
badge: data.badge || '/badge-96.png', // 상태바 작은 아이콘 = 흰 M 단색(투명 배경)
image: data.image || undefined, // big picture (알림 확장 영역의 큰 이미지)
tag: data.tag || undefined,
renotify: !!data.tag,
requireInteraction: true,
silent: false,
vibrate: [200, 100, 200],
timestamp: Date.now(),
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);
})
);
});