fix(mobile): 뒤로가기 토스트가 페이지 이동 후에도 작동하도록
Deploy momo-erp / deploy (push) Successful in 2m59s

기존: 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:
chpark
2026-05-13 00:45:14 +09:00
parent 1e0a2640e9
commit bfb9470c85
+21 -30
View File
@@ -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;
}