From 21c8bf5ab55be3542a75484afd33e60a49b83ca1 Mon Sep 17 00:00:00 2001 From: chpark Date: Wed, 27 May 2026 00:39:18 +0900 Subject: [PATCH] =?UTF-8?q?fix(push,profile):=20=ED=91=B8=EC=8B=9C=20?= =?UTF-8?q?=EC=A7=84=EB=8B=A8(=ED=99=98=EC=98=81/=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8/=EC=B9=B4=EC=9A=B4=ED=8A=B8)=20+=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EB=8B=AB=EA=B8=B0=20=EB=B2=84=ED=8A=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 푸시: - 구독 직후 '환영 푸시' 자동 발송 — 서버→푸시서비스→기기 경로 즉시 확인. - /api/m/push/test (GET 구독 카운트, POST 본인 기기 테스트 발송). - PushOptIn: 허용 결과 안내 + '알림 켜짐' 옆 [테스트] 버튼. - sendPush 발송 로그(targets/sent/failed) 추가. 프로필: - 회원정보 수정 페이지에 [닫기] 버튼 — 앱(standalone)은 브라우저 뒤로가기가 없어 모달처럼 갇히던 문제. history 있으면 back, 없으면 /m/orders/new. --- src/app/(main)/profile/page.tsx | 18 ++++++++++++-- src/app/api/m/push/subscribe/route.ts | 10 ++++++-- src/app/api/m/push/test/route.ts | 22 +++++++++++++++++ src/components/push-optin.tsx | 35 ++++++++++++++++++++++++--- src/lib/push.ts | 15 ++++++++++++ 5 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 src/app/api/m/push/test/route.ts diff --git a/src/app/(main)/profile/page.tsx b/src/app/(main)/profile/page.tsx index 3250910..56b56c9 100644 --- a/src/app/(main)/profile/page.tsx +++ b/src/app/(main)/profile/page.tsx @@ -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(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 (
-

회원정보 수정

+
+

회원정보 수정

+ +
{/* 기본 정보 */}
diff --git a/src/app/api/m/push/subscribe/route.ts b/src/app/api/m/push/subscribe/route.ts index 82bba8d..c6a7c7b 100644 --- a/src/app/api/m/push/subscribe/route.ts +++ b/src/app/api/m/push/subscribe/route.ts @@ -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 }); } diff --git a/src/app/api/m/push/test/route.ts b/src/app/api/m/push/test/route.ts new file mode 100644 index 0000000..b86dde1 --- /dev/null +++ b/src/app/api/m/push/test/route.ts @@ -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 }); +} diff --git a/src/components/push-optin.tsx b/src/components/push-optin.tsx index 4aa303f..a6409cb 100644 --- a/src/components/push-optin.tsx +++ b/src/components/push-optin.tsx @@ -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 ( - - 새 품목 알림 켜짐 + + 알림 켜짐 + ); } diff --git a/src/lib/push.ts b/src/lib/push.ts index c1b4ad8..756b391 100644 --- a/src/lib/push.ts +++ b/src/lib/push.ts @@ -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 }; +}