Skip to content

Commit 2246061

Browse files
committed
fix: 전체 딥링크 수정
1 parent dd7afbc commit 2246061

File tree

3 files changed

+162
-17
lines changed

3 files changed

+162
-17
lines changed

apps/tuk-web/src/app/invite/gathering/[gatheringId]/src/components/InviteGathering.tsx

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,84 @@ import { BackgroundTemplate, Button } from '@/shared/components';
1111
import SkeletonGuard from '@/shared/components/SkeletonGuard';
1212
import { useParam } from '@/shared/hooks/useParam';
1313

14+
const ANDROID_STORE_URL = 'https://play.google.com/store/apps/details?id=com.plottwist.tuk';
15+
const IOS_STORE_URL_WEB =
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 IOS_STORE_URL_APPS = 'itms-apps://itunes.apple.com/app/id6749781762';
18+
19+
function buildAndroidIntent(path: string, playUrl: string) {
20+
return (
21+
`intent://${path}` +
22+
'#Intent;' +
23+
'scheme=tuk-app;' +
24+
'package=com.plottwist.tuk;' +
25+
`S.browser_fallback_url=${encodeURIComponent(playUrl)};` +
26+
'end'
27+
);
28+
}
29+
30+
function openJoinGatheringInApp(gatheringId: number | string) {
31+
const ua = navigator.userAgent.toLowerCase();
32+
const isAndroid = ua.includes('android');
33+
const isIOS = /iphone|ipad|ipod/.test(ua);
34+
35+
// 스킴/경로
36+
const query = `gatheringId=${encodeURIComponent(String(gatheringId))}`;
37+
const schemeUrl = `tuk-app://tuk/join-gathering?${query}`;
38+
39+
if (isAndroid) {
40+
// Android는 intent://로 자동 폴백
41+
const androidPath = `tuk/join-gathering?${query}`;
42+
const intentUrl = buildAndroidIntent(androidPath, ANDROID_STORE_URL);
43+
window.location.href = intentUrl;
44+
return;
45+
}
46+
47+
if (isIOS) {
48+
// iOS: 히든 iframe 시도 → 실패 시 App Store 앱으로 폴백
49+
const fallbackDelayMs = 1500;
50+
let timer: number | null = null;
51+
52+
const cleanup = () => {
53+
if (timer) {
54+
window.clearTimeout(timer);
55+
timer = null;
56+
}
57+
document.removeEventListener('visibilitychange', onHide, true);
58+
window.removeEventListener('pagehide', onHide, true);
59+
window.removeEventListener('blur', onHide, true);
60+
const iframe = document.getElementById('__dl_iframe__');
61+
if (iframe && iframe.parentNode) iframe.parentNode.removeChild(iframe);
62+
};
63+
const onHide = () => cleanup();
64+
65+
document.addEventListener('visibilitychange', onHide, true);
66+
window.addEventListener('pagehide', onHide, true);
67+
window.addEventListener('blur', onHide, true);
68+
69+
const iframe = document.createElement('iframe');
70+
iframe.style.display = 'none';
71+
iframe.id = '__dl_iframe__';
72+
iframe.src = schemeUrl;
73+
document.body.appendChild(iframe);
74+
75+
timer = window.setTimeout(() => {
76+
try {
77+
window.location.replace(IOS_STORE_URL_APPS);
78+
} catch {
79+
window.location.replace(IOS_STORE_URL_WEB);
80+
} finally {
81+
cleanup();
82+
}
83+
}, fallbackDelayMs);
84+
85+
return;
86+
}
87+
88+
// 기타 환경: 웹 스토어로
89+
window.location.href = IOS_STORE_URL_WEB;
90+
}
91+
1492
const InviteGathering = () => {
1593
const gatheringId = Number(useParam('gatheringId'));
1694

@@ -34,9 +112,10 @@ const InviteGathering = () => {
34112
<BackgroundTemplate.CTA>
35113
<Button
36114
className="w-full"
37-
onClick={() =>
38-
(window.location.href = `tuk-app://tuk/join-gathering?gatheringId=${gatheringId}`)
39-
}
115+
onClick={() => {
116+
// 사용자 제스처 내에서 호출
117+
openJoinGatheringInApp(gatheringId);
118+
}}
40119
>
41120
입장하기
42121
</Button>

apps/tuk-web/src/app/invite/meet/[meetId]/src/components/InviteProposal.tsx

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,12 @@ import { BackgroundTemplate, Button } from '@/shared/components';
1111
import SkeletonGuard from '@/shared/components/SkeletonGuard';
1212
import { useParam } from '@/shared/hooks/useParam';
1313

14-
// === Deep Link: 강화 폴백 유틸 시작 ===
1514
const IOS_STORE_URL_WEB =
1615
'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 IOS_STORE_URL_APPS = 'itms-apps://itunes.apple.com/app/id6749781762'; // App Store 앱 직접 오픈
16+
const IOS_STORE_URL_APPS = 'itms-apps://itunes.apple.com/app/id6749781762';
1817
const ANDROID_STORE_URL = 'https://play.google.com/store/apps/details?id=com.plottwist.tuk';
1918

2019
function buildAndroidIntent(path: string, playUrl: string) {
21-
// path 예: "tuk/proposal-detail/123"
2220
return (
2321
`intent://${path}` +
2422
'#Intent;' +
@@ -34,20 +32,17 @@ function openProposalInApp(proposalId: number | string) {
3432
const isAndroid = ua.includes('android');
3533
const isIOS = /iphone|ipad|ipod/.test(ua);
3634

37-
// 공통 스킴 (앱에서 열릴 실제 진입 위치)
3835
const schemeUrl = `tuk-app://tuk/proposal-detail/${encodeURIComponent(String(proposalId))}`;
3936

4037
if (isAndroid) {
41-
// 안드로이드는 기존 intent 유지 (정상 동작 중)
4238
const androidPath = `tuk/proposal-detail/${encodeURIComponent(String(proposalId))}`;
4339
const intentUrl = buildAndroidIntent(androidPath, ANDROID_STORE_URL);
4440
window.location.href = intentUrl;
4541
return;
4642
}
4743

4844
if (isIOS) {
49-
// iOS: 히든 iframe으로 조용히 스킴 시도 → 실패 시 App Store 앱(itms-apps://) 폴백
50-
const fallbackDelayMs = 1500; // 1300~1700 권장
45+
const fallbackDelayMs = 1500;
5146
let timer: number | null = null;
5247

5348
const cleanup = () => {
@@ -62,27 +57,23 @@ function openProposalInApp(proposalId: number | string) {
6257
if (iframe && iframe.parentNode) iframe.parentNode.removeChild(iframe);
6358
};
6459
const onHide = () => {
65-
// 앱으로 전환되면 페이지가 숨김/이탈/블러됨 → 타이머 취소
6660
cleanup();
6761
};
6862

6963
document.addEventListener('visibilitychange', onHide, true);
7064
window.addEventListener('pagehide', onHide, true);
7165
window.addEventListener('blur', onHide, true);
7266

73-
// 히든 iframe으로 스킴 시도 (미설치 시 알럿 없이 조용히 실패)
7467
const iframe = document.createElement('iframe');
7568
iframe.style.display = 'none';
7669
iframe.id = '__dl_iframe__';
7770
iframe.src = schemeUrl;
7871
document.body.appendChild(iframe);
7972

80-
// 전환이 없으면 App Store 앱을 직접 오픈 (itms-apps://)
8173
timer = window.setTimeout(() => {
8274
try {
8375
window.location.replace(IOS_STORE_URL_APPS);
8476
} catch {
85-
// 혹시 itms-apps가 막히면 웹 스토어로 폴백
8677
window.location.replace(IOS_STORE_URL_WEB);
8778
} finally {
8879
cleanup();
@@ -92,10 +83,8 @@ function openProposalInApp(proposalId: number | string) {
9283
return;
9384
}
9485

95-
// 기타 환경(데스크톱 등)은 앱 스토어 웹 페이지로
9686
window.location.href = IOS_STORE_URL_WEB;
9787
}
98-
// === Deep Link: 강화 폴백 유틸 끝 ===
9988

10089
const InviteProposal = () => {
10190
const proposalId = Number(useParam('meetId'));

apps/tuk-web/src/shared/components/AppInstallBanner.tsx

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,83 @@
22

33
import { CloseIcon24 } from '@/shared/components/icon';
44

5+
const ANDROID_PACKAGE = 'com.plottwist.tuk';
6+
const ANDROID_PLAY_WEB = `https://play.google.com/store/apps/details?id=${ANDROID_PACKAGE}`;
7+
const IOS_STORE_URL_APPS = 'itms-apps://itunes.apple.com/app/id6749781762';
8+
const IOS_STORE_URL_WEB =
9+
'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';
10+
11+
// tuk-app://tuk → intent://tuk 로 변환
12+
function buildAndroidIntent(path: string, playUrl: string) {
13+
return (
14+
`intent://${path}` +
15+
'#Intent;' +
16+
'scheme=tuk-app;' +
17+
`package=${ANDROID_PACKAGE};` +
18+
`S.browser_fallback_url=${encodeURIComponent(playUrl)};` +
19+
'end'
20+
);
21+
}
22+
23+
function openAppSmart() {
24+
const ua = navigator.userAgent.toLowerCase();
25+
const isAndroid = ua.includes('android');
26+
const isIOS = /iphone|ipad|ipod/.test(ua);
27+
28+
const schemeUrl = 'tuk-app://tuk'; // 루트 오픈
29+
30+
if (isAndroid) {
31+
// Android: intent:// 으로 자동 폴백
32+
const intentUrl = buildAndroidIntent('tuk', ANDROID_PLAY_WEB);
33+
window.location.href = intentUrl;
34+
return;
35+
}
36+
37+
if (isIOS) {
38+
// iOS: 히든 iframe 시도 → 실패 시 App Store 앱으로 폴백
39+
const fallbackDelayMs = 1500;
40+
let timer: number | null = null;
41+
42+
const cleanup = () => {
43+
if (timer) {
44+
window.clearTimeout(timer);
45+
timer = null;
46+
}
47+
document.removeEventListener('visibilitychange', onHide, true);
48+
window.removeEventListener('pagehide', onHide, true);
49+
window.removeEventListener('blur', onHide, true);
50+
const iframe = document.getElementById('__dl_iframe__');
51+
if (iframe && iframe.parentNode) iframe.parentNode.removeChild(iframe);
52+
};
53+
const onHide = () => cleanup();
54+
55+
document.addEventListener('visibilitychange', onHide, true);
56+
window.addEventListener('pagehide', onHide, true);
57+
window.addEventListener('blur', onHide, true);
58+
59+
const iframe = document.createElement('iframe');
60+
iframe.style.display = 'none';
61+
iframe.id = '__dl_iframe__';
62+
iframe.src = schemeUrl;
63+
document.body.appendChild(iframe);
64+
65+
timer = window.setTimeout(() => {
66+
try {
67+
window.location.replace(IOS_STORE_URL_APPS);
68+
} catch {
69+
window.location.replace(IOS_STORE_URL_WEB);
70+
} finally {
71+
cleanup();
72+
}
73+
}, fallbackDelayMs);
74+
75+
return;
76+
}
77+
78+
// 기타(데스크톱 등): 웹 스토어로
79+
window.location.href = IOS_STORE_URL_WEB;
80+
}
81+
582
const AppInstallBanner = ({ onClose }: { onClose: () => void }) => {
683
return (
784
<div className="fixed left-1/2 top-0 z-20 flex h-[calc(56px+env(safe-area-inset-top))] w-full max-w-[600px] -translate-x-1/2 items-center justify-between bg-gray-50 px-5 pt-[max(0px,env(safe-area-inset-top))] text-black-default shadow-[0_1px_0_0_rgba(0,0,0,0.04)] backdrop-blur-[2px]">
@@ -17,7 +94,7 @@ const AppInstallBanner = ({ onClose }: { onClose: () => void }) => {
1794

1895
<button
1996
className="pretendard-body-12-B rounded-[1.25rem] bg-gray-900 px-2.5 py-2 text-white-default"
20-
onClick={() => (window.location.href = 'tuk-app://tuk')}
97+
onClick={openAppSmart} // ← 여기만 교체
2198
>
2299
앱 열기
23100
</button>

0 commit comments

Comments
 (0)