Skip to content

Commit 33bfcab

Browse files
committed
hotfix: Appstore 링크 연결
1 parent 8ec420f commit 33bfcab

File tree

2 files changed

+157
-1
lines changed

2 files changed

+157
-1
lines changed

apps/web/src/hooks/useStoreLink.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// useStoreLink.ts
2+
import { useEffect, useMemo, useState } from "react";
3+
4+
export type Platform = "iOS" | "Android";
5+
6+
export type Options = {
7+
/** 앱 스킴/유니버설 링크 (있으면 먼저 시도) */
8+
deepLink?: string;
9+
/** 딥링크 실패 시 스토어로 폴백 대기 시간(ms) */
10+
deepLinkTimeoutMs?: number;
11+
/** SSR에서 UA를 주입하고 싶을 때 */
12+
userAgent?: string;
13+
/** 스토어 대신 보여줄 웹 페이지 (선택) */
14+
webFallback?: string;
15+
/** 인앱브라우저(카톡/인스타 등)인 경우 강제 스토어 이동 */
16+
forceStoreOnInApp?: boolean; // default: true
17+
};
18+
19+
function isInAppBrowser(uaRaw?: string): boolean {
20+
const ua = (
21+
uaRaw ?? (typeof navigator !== "undefined" ? navigator.userAgent : "")
22+
).toLowerCase();
23+
24+
// 대표 인앱 브라우저 패턴들
25+
return (
26+
ua.includes("kakaotalk") || // 카카오톡
27+
ua.includes("instagram") || // 인스타그램
28+
ua.includes("fb_iab") || // 페이스북 인앱
29+
ua.includes("fban") ||
30+
ua.includes("fbav") ||
31+
ua.includes("line/") || // 라인
32+
ua.includes("naver(inapp)") || // 네이버앱 인앱
33+
ua.includes("wv") || // Android WebView (크게 잡기)
34+
ua.includes("daumapps") // 다음앱
35+
);
36+
}
37+
38+
function detectPlatform(uaRaw?: string): Platform {
39+
const ua = (
40+
uaRaw ?? (typeof navigator !== "undefined" ? navigator.userAgent : "")
41+
).toLowerCase();
42+
43+
const isiOSLike =
44+
/iphone|ipod|ipad/.test(ua) ||
45+
// iPadOS 13+: Mac처럼 보일 수 있어 터치 포인트로 구분
46+
(typeof navigator !== "undefined" &&
47+
/mac os x|macintosh/.test(ua) &&
48+
"maxTouchPoints" in navigator &&
49+
(navigator as Navigator).maxTouchPoints > 1) ||
50+
// 명시적 macOS → iOS로 취급
51+
/mac os x|macintosh/.test(ua);
52+
53+
if (isiOSLike) return "iOS";
54+
55+
// 명시적 Windows → Android로 취급
56+
if (/windows nt/.test(ua)) return "Android";
57+
58+
// 일반 Android
59+
if (/android/.test(ua)) return "Android";
60+
61+
// 모르면 Android 쪽으로 (요청 사양)
62+
return "Android";
63+
}
64+
65+
/**
66+
* iOS/Android 스토어 링크 훅
67+
* - macOS는 iOS로, Windows는 Android로 매핑
68+
* - 인앱브라우저에서 강제 스토어로 보낼 수 있음
69+
*/
70+
export function useStoreLink(
71+
iosUrl: string,
72+
androidUrl: string,
73+
opts: Options = {}
74+
) {
75+
const {
76+
deepLink,
77+
deepLinkTimeoutMs = 800,
78+
userAgent,
79+
webFallback,
80+
forceStoreOnInApp = true,
81+
} = opts;
82+
83+
const [platform, setPlatform] = useState<Platform>("Android");
84+
const [inApp, setInApp] = useState<boolean>(false);
85+
86+
useEffect(() => {
87+
setPlatform(detectPlatform(userAgent));
88+
setInApp(isInAppBrowser(userAgent));
89+
}, [userAgent]);
90+
91+
const storeUrl = useMemo(
92+
() => (platform === "iOS" ? iosUrl : androidUrl),
93+
[platform, iosUrl, androidUrl]
94+
);
95+
96+
const open = (target: "auto" | "store" | "deeplink" = "auto") => {
97+
if (typeof window === "undefined") return;
98+
99+
const go = (url?: string) => {
100+
if (url) window.location.href = url;
101+
};
102+
103+
// 인앱 브라우저에서 강제 스토어 이동 옵션
104+
if (forceStoreOnInApp && inApp) {
105+
return go(storeUrl || webFallback);
106+
}
107+
108+
const tryDeepLink = () => {
109+
if (!deepLink) return false;
110+
const start = Date.now();
111+
112+
const timer = setTimeout(() => {
113+
// 앱 전환이 없으면 스토어 → 없으면 웹 폴백
114+
if (Date.now() - start < deepLinkTimeoutMs + 100) {
115+
go(storeUrl || webFallback);
116+
}
117+
}, deepLinkTimeoutMs);
118+
119+
const clear = () => clearTimeout(timer);
120+
// 앱이 열려 브라우저가 백그라운드로 가면 blur/visibilitychange 발생
121+
window.addEventListener("blur", clear, { once: true });
122+
document.addEventListener(
123+
"visibilitychange",
124+
() => {
125+
if (document.visibilityState === "hidden") clear();
126+
},
127+
{ once: true }
128+
);
129+
130+
go(deepLink);
131+
return true;
132+
};
133+
134+
if (target === "store") return go(storeUrl || webFallback);
135+
if (target === "deeplink")
136+
return void (tryDeepLink() || go(storeUrl || webFallback));
137+
138+
// auto
139+
if (deepLink) {
140+
if (!tryDeepLink()) go(storeUrl || webFallback);
141+
} else {
142+
go(storeUrl || webFallback);
143+
}
144+
};
145+
146+
return { platform, storeUrl, deepLink, inApp, open };
147+
}

apps/web/src/pages/result/index.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from "../../components/ResultCard/cardConfig";
1313
import { container, downloadSection, buttonContainer } from "./style.css";
1414
import { URLS } from "../../constants/urls";
15+
import { useStoreLink } from "../../hooks/useStoreLink";
1516

1617
const getValidResult = (type: string | null): CardResult | null => {
1718
const validTypes = Object.values(CardResult).find((result) =>
@@ -31,6 +32,10 @@ export const ResultPage = () => {
3132
const navigate = useNavigate();
3233
const [searchParams] = useSearchParams();
3334
const resultCardRef = useRef<HTMLDivElement>(null);
35+
const { open } = useStoreLink(
36+
"https://apps.apple.com/kr/app/gotchai-%EA%B0%93%EC%B1%A0-ai%EB%A5%BC-%EC%B0%BE%EC%95%84%EB%B4%90/id6749149135",
37+
"https://play.google.com/store/apps/details?id=com.turing.gotchai&pli=1"
38+
);
3439

3540
const result = getValidResult(searchParams.get("type"));
3641
const correctCount = getValidCorrectCount(searchParams.get("correct"));
@@ -126,6 +131,10 @@ export const ResultPage = () => {
126131
}
127132
};
128133

134+
const handleClickAppDownload = () => {
135+
open("store");
136+
};
137+
129138
return (
130139
<Layout>
131140
<div className={container}>
@@ -139,7 +148,7 @@ export const ResultPage = () => {
139148
<Text color={COLOR_VARS.green[600]} size="lg" weight="medium">
140149
앱에서도 계속 퀴즈를 풀어볼까요?
141150
</Text>
142-
<Button variant="filled" disabled>
151+
<Button variant="filled" onClick={handleClickAppDownload}>
143152
1분만에 다운로드하기
144153
</Button>
145154
</div>

0 commit comments

Comments
 (0)