fix(push,profile): 푸시 진단(환영/테스트/카운트) + 프로필 닫기 버튼
Deploy momo-erp / deploy (push) Successful in 1m55s

푸시:
- 구독 직후 '환영 푸시' 자동 발송 — 서버→푸시서비스→기기 경로 즉시 확인.
- /api/m/push/test (GET 구독 카운트, POST 본인 기기 테스트 발송).
- PushOptIn: 허용 결과 안내 + '알림 켜짐' 옆 [테스트] 버튼.
- sendPush 발송 로그(targets/sent/failed) 추가.

프로필:
- 회원정보 수정 페이지에 [닫기] 버튼 — 앱(standalone)은 브라우저 뒤로가기가
  없어 모달처럼 갇히던 문제. history 있으면 back, 없으면 /m/orders/new.
This commit is contained in:
chpark
2026-05-27 00:39:18 +09:00
parent 85ac9db997
commit 21c8bf5ab5
5 changed files with 93 additions and 7 deletions
+16 -2
View File
@@ -1,7 +1,8 @@
"use client";
import { useEffect, useState, FormEvent } from "react";
import { Mail, Lock, Building2, User as UserIcon, Phone, FileText, MapPin, Save } from "lucide-react";
import { useRouter } from "next/navigation";
import { Mail, Lock, Building2, User as UserIcon, Phone, FileText, MapPin, Save, X } from "lucide-react";
import Swal from "sweetalert2";
interface Profile {
@@ -17,10 +18,17 @@ interface Profile {
}
export default function ProfilePage() {
const router = useRouter();
const [profile, setProfile] = useState<Profile | null>(null);
const [pwForm, setPwForm] = useState({ current: "", next: "", confirm: "" });
const [loading, setLoading] = useState(false);
// 닫기 — 앱(standalone)에는 브라우저 뒤로가기가 없어서 명시적 닫기 버튼 제공.
const close = () => {
if (typeof window !== "undefined" && window.history.length > 1) router.back();
else router.push("/m/orders/new");
};
const load = async () => {
const res = await fetch("/api/auth/profile");
const j = await res.json();
@@ -96,7 +104,13 @@ export default function ProfilePage() {
return (
<div className="max-w-3xl mx-auto space-y-6">
<h2 className="text-lg font-bold text-gray-800"> </h2>
<div className="flex items-center justify-between">
<h2 className="text-lg font-bold text-gray-800"> </h2>
<button type="button" onClick={close}
className="inline-flex items-center gap-1 h-9 px-3 rounded-lg border border-slate-300 bg-white text-sm font-semibold text-slate-600 hover:bg-slate-50">
<X size={16} />
</button>
</div>
{/* 기본 정보 */}
<form onSubmit={onSaveInfo} className="bg-white border border-slate-200 rounded-xl p-6 space-y-4">
+8 -2
View File
@@ -3,7 +3,7 @@ import { NextRequest, NextResponse } from "next/server";
import { pool } from "@/lib/db";
import { createObjectId } from "@/lib/utils";
import { requireMomoUser } from "@/lib/momo-guard";
import { ensurePushTable } from "@/lib/push";
import { ensurePushTable, sendPush } from "@/lib/push";
export async function POST(req: NextRequest) {
const r = await requireMomoUser();
@@ -33,5 +33,11 @@ export async function POST(req: NextRequest) {
[createObjectId(), userId, endpoint, p256dh, auth, userAgent]
);
return NextResponse.json({ success: true });
// 구독 직후 환영 푸시 — 전체 파이프라인(서버→푸시서비스→기기) 동작 즉시 확인용
const welcome = await sendPush(
{ title: "알림이 켜졌어요 🔔", body: "새 품목이 판매되면 여기로 알려드릴게요.", url: "/m/orders/new" },
[userId]
).catch((e) => { console.error("[push/subscribe welcome]", e); return { sent: 0, failed: 0 }; });
return NextResponse.json({ success: true, welcomeSent: welcome.sent });
}
+22
View File
@@ -0,0 +1,22 @@
// 푸시 진단 — GET: 구독 현황 카운트 / POST: 본인 기기로 테스트 발송
import { NextResponse } from "next/server";
import { requireMomoUser } from "@/lib/momo-guard";
import { pushSubCounts, sendPush } from "@/lib/push";
export async function GET() {
const r = await requireMomoUser();
if (r instanceof NextResponse) return r;
const counts = await pushSubCounts();
return NextResponse.json({ success: true, ...counts });
}
export async function POST() {
const r = await requireMomoUser();
if (r instanceof NextResponse) return r;
const userId = r.user.objid || r.user.userId;
const res = await sendPush(
{ title: "테스트 알림 🔔", body: "이 알림이 보이면 푸시가 정상 동작합니다.", url: "/m/orders/new" },
[userId]
);
return NextResponse.json({ success: true, ...res });
}
+32 -3
View File
@@ -2,6 +2,7 @@
import { useCallback, useEffect, useState } from "react";
import { Bell, BellRing, BellOff } from "lucide-react";
import Swal from "sweetalert2";
// VAPID 공개키(base64url) → Uint8Array
function urlBase64ToUint8Array(base64String: string): Uint8Array {
@@ -66,18 +67,46 @@ export function PushOptIn() {
try {
const p = await Notification.requestPermission();
setPerm(p as Perm);
if (p === "granted") await subscribe();
if (p === "granted") {
const ok = await subscribe();
Swal.fire({
icon: ok ? "success" : "error",
title: ok ? "알림이 켜졌어요" : "알림 등록 실패",
text: ok ? "테스트 알림이 곧 도착합니다." : "잠시 후 다시 시도해주세요.",
timer: ok ? 1800 : undefined,
showConfirmButton: !ok,
});
} else {
Swal.fire({ icon: "warning", title: "알림이 허용되지 않았습니다", text: "기기/브라우저 설정에서 알림을 허용해주세요." });
}
} finally {
setBusy(false);
}
};
const sendTest = async () => {
setBusy(true);
try {
const res = await fetch("/api/m/push/test", { method: "POST" });
const j = await res.json().catch(() => ({}));
Swal.fire({
icon: j?.sent > 0 ? "success" : "warning",
title: j?.sent > 0 ? "테스트 알림 발송됨" : "발송된 알림이 없습니다",
text: j?.sent > 0 ? "잠시 후 알림을 확인하세요." : "구독 정보가 없거나 기기가 알림을 차단했을 수 있습니다.",
});
} finally { setBusy(false); }
};
if (!supported || perm === "unsupported") return null;
if (perm === "granted") {
return (
<span className="inline-flex items-center gap-1 text-xs text-emerald-700 font-semibold">
<BellRing size={14} />
<span className="inline-flex items-center gap-1.5 text-xs text-emerald-700 font-semibold">
<BellRing size={14} />
<button type="button" onClick={sendTest} disabled={busy}
className="ml-1 px-2 py-0.5 rounded bg-emerald-50 text-emerald-700 hover:bg-emerald-100 disabled:opacity-50 text-[11px] font-bold">
</button>
</span>
);
}
+15
View File
@@ -125,5 +125,20 @@ export async function sendPush(
await pool.query(`DELETE FROM momo_push_subscriptions WHERE objid IN (${ph})`, stale).catch(() => {});
}
console.log(`[push] targets=${rows.length} sent=${sent} failed=${failed} stale=${stale.length} title="${payload.title}"`);
return { sent, failed };
}
// 구독 현황 카운트 (진단용)
export async function pushSubCounts(): Promise<{ total: number; general: number; admin: number }> {
await ensurePushTable();
const res = await pool.query<{ total: string; admin: string }>(
`SELECT COUNT(*)::int::text AS total,
COUNT(*) FILTER (WHERE UPPER(COALESCE(u.user_type,'')) = 'A')::int::text AS admin
FROM momo_push_subscriptions s
LEFT JOIN user_info u ON u.user_id = s.user_id`
);
const total = Number(res.rows[0]?.total ?? 0);
const admin = Number(res.rows[0]?.admin ?? 0);
return { total, general: total - admin, admin };
}