diff --git a/public/sw.js b/public/sw.js
index 7d5af01..8901f19 100644
--- a/public/sw.js
+++ b/public/sw.js
@@ -1,5 +1,5 @@
// 모모유통 ERP — Service Worker (PWA install criteria 충족용)
-const CACHE = 'momo-erp-v3';
+const CACHE = 'momo-erp-v4';
const PRECACHE = ['/', '/manifest.json', '/icon-192.png', '/icon-512.png', '/badge-96.png'];
self.addEventListener('install', (e) => {
@@ -37,9 +37,10 @@ self.addEventListener('push', (e) => {
try { data = e.data ? e.data.json() : {}; } catch (_) { data = {}; }
const title = data.title || '모모유통';
const options = {
- body: data.body || '',
+ 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,
diff --git a/src/app/(main)/m/admin/notices/page.tsx b/src/app/(main)/m/admin/notices/page.tsx
index aad357d..daf9491 100644
--- a/src/app/(main)/m/admin/notices/page.tsx
+++ b/src/app/(main)/m/admin/notices/page.tsx
@@ -199,6 +199,11 @@ export default function AdminNoticesPage() {
const groupNames = activeGroup && !sendAll ? [activeGroup.NAME] : [];
// 푸시 본문은 plain text — HTML 태그 제거 + 공백 압축
const plainBody = bodyText.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/\s+/g, " ").trim();
+ // 첨부 이미지가 없으면 본문 HTML 의 첫
추출해서 big picture 로 사용
+ const firstImgInBody = (() => {
+ const m = bodyText.match(/
]+src=["']([^"']+)["']/i);
+ return m ? m[1] : undefined;
+ })();
const sendRes = await fetch("/api/m/admin/notices/send-push", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({
@@ -207,6 +212,7 @@ export default function AdminNoticesPage() {
userIds: sendAll ? undefined : targetUserIds,
sendAll,
groupNames,
+ imageUrl: imageUrl || firstImgInBody,
}),
});
const sj = await sendRes.json();
diff --git a/src/app/api/m/admin/notices/send-push/route.ts b/src/app/api/m/admin/notices/send-push/route.ts
index 98796a9..5effdf1 100644
--- a/src/app/api/m/admin/notices/send-push/route.ts
+++ b/src/app/api/m/admin/notices/send-push/route.ts
@@ -12,7 +12,7 @@ export async function POST(req: NextRequest) {
await ensureNoticesTable();
const body = await req.json().catch(() => ({}));
- const { noticeObjid, title, message, url, userIds, sendAll, groupNames } = body as {
+ const { noticeObjid, title, message, url, userIds, sendAll, groupNames, imageUrl } = body as {
noticeObjid?: string;
title?: string;
message?: string;
@@ -20,6 +20,7 @@ export async function POST(req: NextRequest) {
userIds?: string[];
sendAll?: boolean;
groupNames?: string[];
+ imageUrl?: string;
};
if (!title || !title.trim()) {
return NextResponse.json({ success: false, message: "제목은 필수입니다." }, { status: 400 });
@@ -33,8 +34,25 @@ export async function POST(req: NextRequest) {
: noticeObjid ? `/m/notices/${noticeObjid}`
: "/m/orders/new";
+ // 이미지 URL 절대화 — FCM/web-push 가 외부에서 접근 가능한 절대 URL 필요
+ const absImage = (() => {
+ if (!imageUrl) return undefined;
+ if (/^https?:\/\//i.test(imageUrl)) return imageUrl;
+ const base = process.env.NEXTAUTH_URL || "https://momotogether.com";
+ return imageUrl.startsWith("/") ? `${base}${imageUrl}` : `${base}/${imageUrl}`;
+ })();
+
+ // 본문이 비어있으면 placeholder ("새 공지 도착") — Android 가 빈 body 알림을 무시하는 케이스 회피
+ const finalBody = (message ?? "").trim().slice(0, 240) || "(공지 페이지에서 확인)";
+
const res = await sendPush(
- { title: title.trim(), body: (message ?? "").slice(0, 240), url: targetUrl, tag: noticeObjid ? `notice-${noticeObjid}` : undefined },
+ {
+ title: title.trim(),
+ body: finalBody,
+ url: targetUrl,
+ tag: noticeObjid ? `notice-${noticeObjid}` : undefined,
+ image: absImage,
+ },
sendAll ? undefined : targets
);
diff --git a/src/lib/firebase-push.ts b/src/lib/firebase-push.ts
index f1ba36c..d47f4cc 100644
--- a/src/lib/firebase-push.ts
+++ b/src/lib/firebase-push.ts
@@ -87,6 +87,7 @@ export interface FcmPayload {
body: string;
url?: string;
tag?: string;
+ image?: string; // big picture URL (절대 URL 권장)
}
// FCM v1 send — 토큰 1개씩 호출 (v1 은 batch 미지원).
@@ -105,17 +106,19 @@ export async function sendFcm(tokens: string[], payload: FcmPayload): Promise {
- const message = {
+ const message: Record = {
token,
notification: {
title: payload.title,
- body: payload.body,
+ body: payload.body || " ", // 빈 본문 대비 — 일부 OS 가 body 없으면 알림을 dismiss
+ ...(payload.image ? { image: payload.image } : {}),
},
data: {
title: payload.title,
- body: payload.body,
+ body: payload.body || "",
url: payload.url || "/m/orders/new",
tag: payload.tag || "",
+ ...(payload.image ? { image: payload.image } : {}),
},
android: {
priority: "HIGH" as const,
@@ -123,8 +126,11 @@ export async function sendFcm(tokens: string[], payload: FcmPayload): Promise