@@ -11,6 +11,86 @@ import { BackgroundTemplate, Button } from '@/shared/components';
1111import SkeletonGuard from '@/shared/components/SkeletonGuard' ;
1212import { useParam } from '@/shared/hooks/useParam' ;
1313
14+ // === Deep Link: 강화 폴백 유틸 시작 ===
15+ const IOS_STORE_URL =
16+ 'https://apps.apple.com/kr/app/%ED%88%AD-%EB%A7%8C%EB%82%8C%EC%9D%84-%EB%84%8C%EC%A7%80%EC%8B%9C/id6749781762' ;
17+ const ANDROID_STORE_URL = 'https://play.google.com/store/apps/details?id=com.plottwist.tuk' ;
18+
19+ function buildAndroidIntent ( path : string , playUrl : string ) {
20+ // path 예: "tuk/proposal-detail/123"
21+ return (
22+ `intent://${ path } ` +
23+ '#Intent;' +
24+ 'scheme=tuk-app;' +
25+ 'package=com.plottwist.tuk;' +
26+ `S.browser_fallback_url=${ encodeURIComponent ( playUrl ) } ;` +
27+ 'end'
28+ ) ;
29+ }
30+
31+ function openProposalInApp ( proposalId : number | string ) {
32+ const ua = navigator . userAgent . toLowerCase ( ) ;
33+ const isAndroid = ua . includes ( 'android' ) ;
34+
35+ // 인앱 브라우저(대략) 감지 — 카톡/인스타 등
36+ const isInApp = / \b ( k a k a o t a l k | i n s t a g r a m | l i n e | f b _ i a b | f b a v | t w i t t e r | n a v e r ( i n a p p ) ? | d a u m a p p s ) \b / . test (
37+ ua
38+ ) ;
39+
40+ // 공통 스킴 (앱에서 열릴 실제 진입 위치)
41+ const iosScheme = `tuk-app://tuk/proposal-detail/${ encodeURIComponent ( String ( proposalId ) ) } ` ;
42+ const androidPath = `tuk/proposal-detail/${ encodeURIComponent ( String ( proposalId ) ) } ` ;
43+
44+ // Android: 가능하면 intent:// 로 한 방에 폴백 자동 처리
45+ if ( isAndroid ) {
46+ const intentUrl = buildAndroidIntent ( androidPath , ANDROID_STORE_URL ) ;
47+ // 일부 인앱 브라우저는 intent 처리에 제약이 있어도, 시도 → 스토어 폴백은 작동
48+ window . location . href = intentUrl ;
49+ return ;
50+ }
51+
52+ // iOS (및 기타): 스킴 시도 → 전환 신호 없으면 스토어로 replace
53+ const fallbackDelayMs = 1700 ; // 1500~1800 권장
54+ let timer : number | null = null ;
55+
56+ const cleanup = ( ) => {
57+ if ( timer ) {
58+ window . clearTimeout ( timer ) ;
59+ timer = null ;
60+ }
61+ document . removeEventListener ( 'visibilitychange' , onHide , true ) ;
62+ window . removeEventListener ( 'pagehide' , onHide , true ) ;
63+ window . removeEventListener ( 'blur' , onHide , true ) ;
64+ } ;
65+ const onHide = ( ) => {
66+ // 앱으로 전환되면 페이지가 숨김/이탈/블러됨 → 타이머 취소
67+ cleanup ( ) ;
68+ } ;
69+
70+ document . addEventListener ( 'visibilitychange' , onHide , true ) ;
71+ window . addEventListener ( 'pagehide' , onHide , true ) ;
72+ window . addEventListener ( 'blur' , onHide , true ) ;
73+
74+ // 인앱 브라우저일 때 커스텀 스킴이 막히는 케이스가 흔하지만
75+ // 그래도 한번 시도 후 폴백(UX 유지)
76+ try {
77+ window . location . href = iosScheme ;
78+ } catch {
79+ /* empty */
80+ }
81+
82+ timer = window . setTimeout (
83+ ( ) => {
84+ // 전환 신호 없으면 미설치/차단으로 판단 → 스토어로 이동
85+ // replace를 써서 '뒤로 가기' 시 빈 페이지/루프 방지
86+ window . location . replace ( IOS_STORE_URL ) ;
87+ cleanup ( ) ;
88+ } ,
89+ isInApp ? 1200 : fallbackDelayMs
90+ ) ; // 인앱 브라우저는 약간 더 짧게
91+ }
92+ // === Deep Link: 강화 폴백 유틸 끝 ===
93+
1494const InviteProposal = ( ) => {
1595 const proposalId = Number ( useParam ( 'meetId' ) ) ;
1696
@@ -34,7 +114,10 @@ const InviteProposal = () => {
34114 < BackgroundTemplate . CTA >
35115 < Button
36116 className = "w-full"
37- onClick = { ( ) => ( window . location . href = `tuk-app://tuk/proposal-detail/${ proposalId } ` ) }
117+ onClick = { ( ) => {
118+ // 사용자 제스처 내부에서 호출해야 브라우저가 내비게이션 차단 안 함
119+ openProposalInApp ( proposalId ) ;
120+ } }
38121 >
39122 초대장 확인하기
40123 </ Button >
0 commit comments