fix(push,profile): 푸시 진단(환영/테스트/카운트) + 프로필 닫기 버튼
Deploy momo-erp / deploy (push) Successful in 1m55s
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:
@@ -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">
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user