기존: mount 시 한 번만 history sentinel push → 사용자가 navigation 하면 sentinel 잃어버려 토스트 안 뜸. 변경: usePathname 의존성 useEffect → pathname 변경마다 sentinel 새로 push. lastBackRef 도 ref 로 변경(렌더 의존성 없이 상태 유지). + swal toast z-index 9999 강제 (다른 모달 위) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,75 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
/**
|
||||
* TWA/PWA standalone 환경에서 안드로이드 뒤로가기 처리.
|
||||
* - 뒤로 갈 history 가 없을 때(또는 시작 entry 일 때) 첫 번째 뒤로가기는 토스트만 띄움
|
||||
* - 2초 안에 한 번 더 뒤로가기 누르면 native 가 처리하도록 history.back() 호출 → 앱 종료
|
||||
*
|
||||
* 일반 데스크톱 브라우저 사용자에게는 영향 없음 (standalone 모드에서만 활성화).
|
||||
* TWA/PWA standalone 또는 모바일 환경에서 안드로이드 뒤로가기 처리.
|
||||
* - 더 갈 history 가 없을 때(또는 sentinel entry 일 때) 첫 번째 뒤로가기는 토스트만
|
||||
* - 2초 안에 한 번 더 뒤로 누르면 native 가 처리하도록 history.back() → 앱 종료
|
||||
* - pathname 이 바뀔 때마다 sentinel 을 새로 push 해서 navigation 후에도 작동
|
||||
*/
|
||||
export default function BackButtonGuard() {
|
||||
const pathname = usePathname();
|
||||
const lastBackRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
// standalone(PWA/TWA) 이거나 모바일 UA 면 활성화
|
||||
// (TWA 일부 환경에서 display-mode 가 standalone 으로 보고되지 않는 케이스 대응)
|
||||
const isStandalone =
|
||||
window.matchMedia("(display-mode: standalone)").matches ||
|
||||
window.matchMedia("(display-mode: fullscreen)").matches ||
|
||||
window.matchMedia("(display-mode: minimal-ui)").matches ||
|
||||
(window.navigator as Navigator & { standalone?: boolean }).standalone === true;
|
||||
const isMobileUA = /Mobile|Android|iPhone|iPad|iPod/i.test(window.navigator.userAgent);
|
||||
|
||||
if (!isStandalone && !isMobileUA) return;
|
||||
|
||||
let lastBackPress = 0;
|
||||
let toastTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// history 에 dummy entry 1 개 추가 → 첫 뒤로가기가 이 dummy 로 pop
|
||||
history.pushState({ __back_guard: true }, "", location.href);
|
||||
// pathname 진입(또는 변경) 시마다 sentinel 1 개 push — webview history 의 마지막 자리에 두기 위함
|
||||
history.pushState({ __back_guard: true, ts: Date.now() }, "", location.href);
|
||||
|
||||
const onPopState = () => {
|
||||
const now = Date.now();
|
||||
if (now - lastBackPress < 2000) {
|
||||
// 2초 이내 두 번째 뒤로 → native 가 처리하도록 한 번 더 back (앱 종료)
|
||||
if (toastTimer) {
|
||||
clearTimeout(toastTimer);
|
||||
toastTimer = null;
|
||||
}
|
||||
if (now - lastBackRef.current < 2000) {
|
||||
// 2초 이내 두 번째 뒤로 → 진짜 종료 (history.back 으로 native back 트리거)
|
||||
history.back();
|
||||
return;
|
||||
}
|
||||
lastBackPress = now;
|
||||
lastBackRef.current = now;
|
||||
|
||||
// dummy entry 다시 push (다음 뒤로가기를 다시 가로채려고)
|
||||
history.pushState({ __back_guard: true }, "", location.href);
|
||||
// sentinel 다시 push
|
||||
history.pushState({ __back_guard: true, ts: Date.now() }, "", location.href);
|
||||
|
||||
// 토스트 표시 — sweetalert2 의 toast mode
|
||||
Swal.fire({
|
||||
toast: true,
|
||||
position: "bottom",
|
||||
showConfirmButton: false,
|
||||
timer: 1800,
|
||||
timer: 2000,
|
||||
timerProgressBar: true,
|
||||
icon: "info",
|
||||
title: "한 번 더 누르면 앱이 종료됩니다",
|
||||
background: "#1f2937",
|
||||
color: "#ffffff",
|
||||
customClass: { popup: "z-[9999]" },
|
||||
});
|
||||
|
||||
toastTimer = setTimeout(() => {
|
||||
lastBackPress = 0;
|
||||
setTimeout(() => {
|
||||
lastBackRef.current = 0;
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
window.addEventListener("popstate", onPopState);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("popstate", onPopState);
|
||||
if (toastTimer) clearTimeout(toastTimer);
|
||||
};
|
||||
}, []);
|
||||
}, [pathname]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user