From fbe9bd5717dce29652e8039972fa13b4584d33d3 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 20 Feb 2026 21:51:04 +0900 Subject: [PATCH 01/20] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4?= =?UTF-8?q?=ED=86=A1=20=EC=9D=B8=EC=95=B1=20=EB=B8=8C=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=EC=A0=80=20=EA=B0=90=EC=A7=80=20=EC=9C=A0=ED=8B=B8=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/utils/isKakaoTalkBrowser.ts | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 frontend/src/utils/isKakaoTalkBrowser.ts diff --git a/frontend/src/utils/isKakaoTalkBrowser.ts b/frontend/src/utils/isKakaoTalkBrowser.ts new file mode 100644 index 000000000..9880fcac6 --- /dev/null +++ b/frontend/src/utils/isKakaoTalkBrowser.ts @@ -0,0 +1,3 @@ +const isKakaoTalkBrowser = () => /KAKAOTALK/i.test(navigator.userAgent); + +export default isKakaoTalkBrowser; From 06c137c8c389828d806a56e03bd8243109add510 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 20 Feb 2026 21:51:13 +0900 Subject: [PATCH 02/20] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4?= =?UTF-8?q?=ED=86=A1=EC=97=90=EC=84=9C=20=EC=95=B1=20=EC=97=B4=EA=B8=B0=20?= =?UTF-8?q?=EC=9C=A0=ED=8B=B8=20=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Android: intent URL 스킴으로 앱 실행 (미설치 시 Play Store 이동) - iOS: 카카오톡 외부 브라우저로 열어 Universal Link 트리거 --- frontend/src/utils/openAppFromKakao.ts | 32 ++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 frontend/src/utils/openAppFromKakao.ts diff --git a/frontend/src/utils/openAppFromKakao.ts b/frontend/src/utils/openAppFromKakao.ts new file mode 100644 index 000000000..1f72e91bf --- /dev/null +++ b/frontend/src/utils/openAppFromKakao.ts @@ -0,0 +1,32 @@ +import { detectPlatform } from './appStoreLink'; + +const ANDROID_PACKAGE = 'com.moadong.moadong'; + +/** + * 카카오톡 인앱 브라우저에서 앱을 여는 함수. + * - Android: intent URL 스킴으로 앱 실행 (미설치 시 Play Store 이동) + * - iOS: 외부 Safari로 열어 Universal Link 트리거 + */ +const openAppFromKakao = (path?: string) => { + const platform = detectPlatform(); + const currentUrl = path ?? window.location.href; + + if (platform === 'Android') { + const url = new URL(currentUrl); + const intentUrl = + `intent://${url.host}${url.pathname}${url.search}${url.hash}` + + `#Intent;scheme=https;package=${ANDROID_PACKAGE};end`; + window.location.href = intentUrl; + return; + } + + if (platform === 'iOS') { + const safariUrl = `kakaotalk://web/openExternal?url=${encodeURIComponent(currentUrl)}`; + window.location.href = safariUrl; + return; + } + + window.location.href = currentUrl; +}; + +export default openAppFromKakao; From 1af18cfdc760866f954b3be70f46e22353aee777 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 20 Feb 2026 21:51:28 +0900 Subject: [PATCH 03/20] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4?= =?UTF-8?q?=ED=86=A1=20=EC=9D=B8=EC=95=B1=20=EB=B8=8C=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=EC=A0=80=EC=97=90=EC=84=9C=20=EC=95=B1=EC=97=B4=EA=B8=B0=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 앱 웹뷰 / 카카오톡 / 일반 브라우저 3단계 조건 분기 적용 - 카카오톡 환경에서만 앱열기 버튼 노출 --- .../components/ClubDetailTopBar/ClubDetailTopBar.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx b/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx index 5fdafacde..776e193b3 100644 --- a/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx +++ b/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx @@ -5,6 +5,8 @@ import NotificationIcon from '@/assets/images/icons/notification_icon.svg?react' import PrevButtonIcon from '@/assets/images/icons/prev_button_icon.svg?react'; import { useScrollTrigger } from '@/hooks/Scroll/useScrollTrigger'; import isInAppWebView from '@/utils/isInAppWebView'; +import isKakaoTalkBrowser from '@/utils/isKakaoTalkBrowser'; +import openAppFromKakao from '@/utils/openAppFromKakao'; import { requestNavigateBack, requestNotificationSubscribe, @@ -43,6 +45,7 @@ const ClubDetailTopBar = ({ const navigate = useNavigate(); const theme = useTheme(); const isInApp = isInAppWebView(); + const isKakao = !isInApp && isKakaoTalkBrowser(); const [isNotificationActive, setIsNotificationActive] = useState(initialIsSubscribed); @@ -118,6 +121,10 @@ const ClubDetailTopBar = ({ /> + ) : isKakao ? ( + openAppFromKakao()}> + 앱열기 + ) : ( )} From ea5c9ad546e5cd6cecb683eedd3774ff12da610e Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 20 Feb 2026 21:51:33 +0900 Subject: [PATCH 04/20] =?UTF-8?q?style:=20AppOpenButton=20pill=20=ED=98=95?= =?UTF-8?q?=ED=83=9C=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ClubDetailTopBar.styles.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.styles.ts b/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.styles.ts index 6bffbb6bd..90557abe0 100644 --- a/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.styles.ts +++ b/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.styles.ts @@ -111,3 +111,21 @@ export const TabButton = styled.button<{ $active: boolean }>` cursor: pointer; transition: all 0.2s ease; `; + +export const AppOpenButton = styled.button` + padding: 6px 12px; + border: none; + background-color: ${({ theme }) => theme.colors.primary[900]}; + color: ${({ theme }) => theme.colors.base.white}; + font-size: 13px; + font-weight: 600; + border-radius: 18px; + cursor: pointer; + white-space: nowrap; + font-family: 'Pretendard', sans-serif; + transition: opacity 0.2s ease; + + &:active { + opacity: 0.7; + } +`; From 2c64d9269a6c1d7dfb10459bbb894378299b1a06 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 20 Feb 2026 21:58:12 +0900 Subject: [PATCH 05/20] =?UTF-8?q?refactor:=20Android=20=EC=95=B1=20?= =?UTF-8?q?=EC=97=B4=EA=B8=B0=20=EB=B0=A9=EC=8B=9D=EC=9D=84=20=EC=99=B8?= =?UTF-8?q?=EB=B6=80=20=EB=B8=8C=EB=9D=BC=EC=9A=B0=EC=A0=80=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=EC=9C=BC=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - intent URL 스킴 대신 kakaotalk://web/openExternal로 외부 브라우저 열기 - Android App Links(assetlinks.json) 기반으로 바로 앱 실행되도록 개선 - iOS/Android 동일한 로직으로 단순화 --- frontend/src/utils/openAppFromKakao.ts | 30 +++++--------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/frontend/src/utils/openAppFromKakao.ts b/frontend/src/utils/openAppFromKakao.ts index 1f72e91bf..404402ecb 100644 --- a/frontend/src/utils/openAppFromKakao.ts +++ b/frontend/src/utils/openAppFromKakao.ts @@ -1,32 +1,12 @@ -import { detectPlatform } from './appStoreLink'; - -const ANDROID_PACKAGE = 'com.moadong.moadong'; - /** - * 카카오톡 인앱 브라우저에서 앱을 여는 함수. - * - Android: intent URL 스킴으로 앱 실행 (미설치 시 Play Store 이동) - * - iOS: 외부 Safari로 열어 Universal Link 트리거 + * 카카오톡 인앱 브라우저에서 외부 브라우저로 열어 앱을 실행하는 함수. + * - Android: Chrome으로 열어 Android App Links 트리거 (assetlinks.json 설정 필요) + * - iOS: Safari로 열어 Universal Link 트리거 (apple-app-site-association 설정 필요) */ const openAppFromKakao = (path?: string) => { - const platform = detectPlatform(); const currentUrl = path ?? window.location.href; - - if (platform === 'Android') { - const url = new URL(currentUrl); - const intentUrl = - `intent://${url.host}${url.pathname}${url.search}${url.hash}` + - `#Intent;scheme=https;package=${ANDROID_PACKAGE};end`; - window.location.href = intentUrl; - return; - } - - if (platform === 'iOS') { - const safariUrl = `kakaotalk://web/openExternal?url=${encodeURIComponent(currentUrl)}`; - window.location.href = safariUrl; - return; - } - - window.location.href = currentUrl; + const externalUrl = `kakaotalk://web/openExternal?url=${encodeURIComponent(currentUrl)}`; + window.location.href = externalUrl; }; export default openAppFromKakao; From d07370dbd6f9893430dc4f575d33ffb9c80a22c3 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 20 Feb 2026 22:03:21 +0900 Subject: [PATCH 06/20] =?UTF-8?q?feat:=20Android=20=EB=AF=B8=EC=84=A4?= =?UTF-8?q?=EC=B9=98=20=EC=8B=9C=20Play=20Store=20=ED=8F=B4=EB=B0=B1=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - intent URL에 S.browser_fallback_url 파라미터 추가 - 앱 설치 시 바로 실행, 미설치 시 Play Store로 자동 리다이렉트 - iOS는 Safari 외부 브라우저 방식 유지 --- frontend/src/utils/openAppFromKakao.ts | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/frontend/src/utils/openAppFromKakao.ts b/frontend/src/utils/openAppFromKakao.ts index 404402ecb..811089021 100644 --- a/frontend/src/utils/openAppFromKakao.ts +++ b/frontend/src/utils/openAppFromKakao.ts @@ -1,12 +1,28 @@ +import { APP_STORE_LINKS, detectPlatform } from './appStoreLink'; + +const ANDROID_PACKAGE = 'com.moadong.moadong'; + /** - * 카카오톡 인앱 브라우저에서 외부 브라우저로 열어 앱을 실행하는 함수. - * - Android: Chrome으로 열어 Android App Links 트리거 (assetlinks.json 설정 필요) - * - iOS: Safari로 열어 Universal Link 트리거 (apple-app-site-association 설정 필요) + * 카카오톡 인앱 브라우저에서 앱을 실행하는 함수. + * - Android: intent URL로 앱 직접 실행, 미설치 시 Play Store 이동 + * - iOS: Safari로 열어 Universal Link 트리거, 미설치 시 App Store 이동 */ const openAppFromKakao = (path?: string) => { + const platform = detectPlatform(); const currentUrl = path ?? window.location.href; - const externalUrl = `kakaotalk://web/openExternal?url=${encodeURIComponent(currentUrl)}`; - window.location.href = externalUrl; + + if (platform === 'Android') { + const url = new URL(currentUrl); + const fallback = encodeURIComponent(APP_STORE_LINKS.android); + const intentUrl = + `intent://${url.host}${url.pathname}${url.search}${url.hash}` + + `#Intent;scheme=https;package=${ANDROID_PACKAGE};S.browser_fallback_url=${fallback};end`; + window.location.href = intentUrl; + return; + } + + const safariUrl = `kakaotalk://web/openExternal?url=${encodeURIComponent(currentUrl)}`; + window.location.href = safariUrl; }; export default openAppFromKakao; From 9d9d947dd04eeff1b1e2bddbf3133f7e6e4d51e9 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 20 Feb 2026 22:15:50 +0900 Subject: [PATCH 07/20] =?UTF-8?q?fix:=20Android=20intent=20URL=20host?= =?UTF-8?q?=EB=A5=BC=20=ED=94=84=EB=A1=9C=EB=8D=95=EC=85=98=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=EC=9C=BC=EB=A1=9C=20=EA=B3=A0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/utils/openAppFromKakao.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/utils/openAppFromKakao.ts b/frontend/src/utils/openAppFromKakao.ts index 811089021..f3a3fd859 100644 --- a/frontend/src/utils/openAppFromKakao.ts +++ b/frontend/src/utils/openAppFromKakao.ts @@ -1,6 +1,7 @@ import { APP_STORE_LINKS, detectPlatform } from './appStoreLink'; const ANDROID_PACKAGE = 'com.moadong.moadong'; +const ANDROID_HOST = 'www.moadong.com'; /** * 카카오톡 인앱 브라우저에서 앱을 실행하는 함수. @@ -15,7 +16,7 @@ const openAppFromKakao = (path?: string) => { const url = new URL(currentUrl); const fallback = encodeURIComponent(APP_STORE_LINKS.android); const intentUrl = - `intent://${url.host}${url.pathname}${url.search}${url.hash}` + + `intent://${ANDROID_HOST}${url.pathname}${url.search}${url.hash}` + `#Intent;scheme=https;package=${ANDROID_PACKAGE};S.browser_fallback_url=${fallback};end`; window.location.href = intentUrl; return; From dc1de10490d691e056f8542dbc660594cce9a47a Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 20 Feb 2026 22:25:06 +0900 Subject: [PATCH 08/20] =?UTF-8?q?fix:=20iOS=20=EC=95=B1=20=EC=97=B4?= =?UTF-8?q?=EA=B8=B0=20=ED=94=84=EB=A1=9C=EB=8D=95=EC=85=98=20URL=20?= =?UTF-8?q?=EA=B3=A0=EC=A0=95=20=EB=B0=8F=20App=20Store=20=ED=8F=B4?= =?UTF-8?q?=EB=B0=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - iOS에서도 프로덕션 도메인(www.moadong.com) URL을 사용하도록 buildProductionUrl 추가 - kakaotalk://web/openExternal 실패 시 1.5초 후 App Store로 자동 이동 - ANDROID_HOST를 APP_HOST로 통합하여 양 플랫폼 공통 사용 --- frontend/src/utils/openAppFromKakao.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/frontend/src/utils/openAppFromKakao.ts b/frontend/src/utils/openAppFromKakao.ts index f3a3fd859..ebdad356f 100644 --- a/frontend/src/utils/openAppFromKakao.ts +++ b/frontend/src/utils/openAppFromKakao.ts @@ -1,12 +1,17 @@ import { APP_STORE_LINKS, detectPlatform } from './appStoreLink'; const ANDROID_PACKAGE = 'com.moadong.moadong'; -const ANDROID_HOST = 'www.moadong.com'; +const APP_HOST = 'www.moadong.com'; + +const buildProductionUrl = (currentUrl: string): string => { + const url = new URL(currentUrl); + return `https://${APP_HOST}${url.pathname}${url.search}${url.hash}`; +}; /** * 카카오톡 인앱 브라우저에서 앱을 실행하는 함수. * - Android: intent URL로 앱 직접 실행, 미설치 시 Play Store 이동 - * - iOS: Safari로 열어 Universal Link 트리거, 미설치 시 App Store 이동 + * - iOS: Safari로 열어 Universal Link 트리거, 실패 시 App Store 이동 */ const openAppFromKakao = (path?: string) => { const platform = detectPlatform(); @@ -16,14 +21,21 @@ const openAppFromKakao = (path?: string) => { const url = new URL(currentUrl); const fallback = encodeURIComponent(APP_STORE_LINKS.android); const intentUrl = - `intent://${ANDROID_HOST}${url.pathname}${url.search}${url.hash}` + + `intent://${APP_HOST}${url.pathname}${url.search}${url.hash}` + `#Intent;scheme=https;package=${ANDROID_PACKAGE};S.browser_fallback_url=${fallback};end`; window.location.href = intentUrl; return; } - const safariUrl = `kakaotalk://web/openExternal?url=${encodeURIComponent(currentUrl)}`; - window.location.href = safariUrl; + const productionUrl = buildProductionUrl(currentUrl); + window.location.href = `kakaotalk://web/openExternal?url=${encodeURIComponent(productionUrl)}`; + + const start = Date.now(); + setTimeout(() => { + if (Date.now() - start < 2000 && !document.hidden) { + window.location.href = APP_STORE_LINKS.iphone; + } + }, 1500); }; export default openAppFromKakao; From 14cac806b036c578877d9833c9f01cffb3bffe4d Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 20 Feb 2026 22:29:19 +0900 Subject: [PATCH 09/20] =?UTF-8?q?fix:=20iOS=20=EB=AF=B8=EC=84=A4=EC=B9=98?= =?UTF-8?q?=20=EC=8B=9C=20App=20Store=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Safari에서 프로덕션 URL 대신 App Store 링크를 직접 열도록 수정 - 설치됨 → App Store에서 "열기", 미설치 → "받기"로 양쪽 케이스 대응 - 동작 불가능했던 document.hidden 기반 타임아웃 폴백 로직 제거 --- frontend/src/utils/openAppFromKakao.ts | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/frontend/src/utils/openAppFromKakao.ts b/frontend/src/utils/openAppFromKakao.ts index ebdad356f..31bf1766c 100644 --- a/frontend/src/utils/openAppFromKakao.ts +++ b/frontend/src/utils/openAppFromKakao.ts @@ -3,15 +3,10 @@ import { APP_STORE_LINKS, detectPlatform } from './appStoreLink'; const ANDROID_PACKAGE = 'com.moadong.moadong'; const APP_HOST = 'www.moadong.com'; -const buildProductionUrl = (currentUrl: string): string => { - const url = new URL(currentUrl); - return `https://${APP_HOST}${url.pathname}${url.search}${url.hash}`; -}; - /** * 카카오톡 인앱 브라우저에서 앱을 실행하는 함수. * - Android: intent URL로 앱 직접 실행, 미설치 시 Play Store 이동 - * - iOS: Safari로 열어 Universal Link 트리거, 실패 시 App Store 이동 + * - iOS: Safari에서 App Store 페이지 열기 (설치됨 → 열기, 미설치 → 받기) */ const openAppFromKakao = (path?: string) => { const platform = detectPlatform(); @@ -27,15 +22,8 @@ const openAppFromKakao = (path?: string) => { return; } - const productionUrl = buildProductionUrl(currentUrl); - window.location.href = `kakaotalk://web/openExternal?url=${encodeURIComponent(productionUrl)}`; - - const start = Date.now(); - setTimeout(() => { - if (Date.now() - start < 2000 && !document.hidden) { - window.location.href = APP_STORE_LINKS.iphone; - } - }, 1500); + const appStoreUrl = APP_STORE_LINKS.iphone; + window.location.href = `kakaotalk://web/openExternal?url=${encodeURIComponent(appStoreUrl)}`; }; export default openAppFromKakao; From 1d109555fa632647a382a9401041e9bfc2a72574 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 20 Feb 2026 22:29:30 +0900 Subject: [PATCH 10/20] =?UTF-8?q?fix:=20=EB=B2=84=ED=8A=BC=20=EC=BB=AC?= =?UTF-8?q?=EB=9F=AC=20=EA=B2=80=EC=9D=80=EC=83=89=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ClubDetailTopBar/ClubDetailTopBar.styles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.styles.ts b/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.styles.ts index 90557abe0..d36bf6fe7 100644 --- a/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.styles.ts +++ b/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.styles.ts @@ -115,7 +115,7 @@ export const TabButton = styled.button<{ $active: boolean }>` export const AppOpenButton = styled.button` padding: 6px 12px; border: none; - background-color: ${({ theme }) => theme.colors.primary[900]}; + background-color: ${({ theme }) => theme.colors.base.black}; color: ${({ theme }) => theme.colors.base.white}; font-size: 13px; font-weight: 600; From 288f82171a43652bbecda248ea1526bc3ad2e497 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 20 Feb 2026 22:41:32 +0900 Subject: [PATCH 11/20] =?UTF-8?q?fix:=20iOS=20=EC=95=B1=20=EC=97=B4?= =?UTF-8?q?=EA=B8=B0=20=EB=B0=A9=EC=8B=9D=EC=9D=84=20Universal=20Link=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Safari에서 프로덕션 URL 열기로 앱 설치 시 바로 실행되도록 변경 - 미설치 시 Smart App Banner를 통해 App Store 유도 --- frontend/src/utils/openAppFromKakao.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/utils/openAppFromKakao.ts b/frontend/src/utils/openAppFromKakao.ts index 31bf1766c..3fa8c36f6 100644 --- a/frontend/src/utils/openAppFromKakao.ts +++ b/frontend/src/utils/openAppFromKakao.ts @@ -6,7 +6,7 @@ const APP_HOST = 'www.moadong.com'; /** * 카카오톡 인앱 브라우저에서 앱을 실행하는 함수. * - Android: intent URL로 앱 직접 실행, 미설치 시 Play Store 이동 - * - iOS: Safari에서 App Store 페이지 열기 (설치됨 → 열기, 미설치 → 받기) + * - iOS: Safari로 열어 Universal Link 트리거 (미설치 시 Smart App Banner 표시) */ const openAppFromKakao = (path?: string) => { const platform = detectPlatform(); @@ -22,8 +22,9 @@ const openAppFromKakao = (path?: string) => { return; } - const appStoreUrl = APP_STORE_LINKS.iphone; - window.location.href = `kakaotalk://web/openExternal?url=${encodeURIComponent(appStoreUrl)}`; + const url = new URL(currentUrl); + const productionUrl = `https://${APP_HOST}${url.pathname}${url.search}${url.hash}`; + window.location.href = `kakaotalk://web/openExternal?url=${encodeURIComponent(productionUrl)}`; }; export default openAppFromKakao; From 12a427ebf87767c4588b7063ef839b2c443a1463 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 20 Feb 2026 22:41:37 +0900 Subject: [PATCH 12/20] =?UTF-8?q?feat:=20iOS=20Safari=20Smart=20App=20Bann?= =?UTF-8?q?er=20=EB=A9=94=ED=83=80=20=ED=83=9C=EA=B7=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/index.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/index.html b/frontend/index.html index c8c0417ca..05aa8fd21 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -18,6 +18,8 @@ content="부경대학교 동아리 찾기, 모집 정보 확인부터 신규 동아리 가입과 홍보까지 한 번에 할 수 있어요." /> + + From 8c2fb3c2122161896ed76cf9a4740cec6d080bf3 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 20 Feb 2026 22:52:42 +0900 Subject: [PATCH 13/20] =?UTF-8?q?feat:=20iOS=20=EC=BB=A4=EC=8A=A4=ED=85=80?= =?UTF-8?q?=20=EC=8A=A4=ED=82=B4=20=EA=B8=B0=EB=B0=98=20=EC=95=B1=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=20=EB=B0=8F=20=EB=AF=B8=EC=84=A4=EC=B9=98=20?= =?UTF-8?q?=EC=8B=9C=20App=20Store=20=ED=8F=B4=EB=B0=B1=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - moadongapp:// 커스텀 스킴으로 앱 직접 실행 시도 - 2초 후에도 페이지가 visible이면 앱 미설치로 판단 → App Store 이동 - 앱이 열려 페이지가 hidden 되면 타이머 취소 --- frontend/src/utils/openAppFromKakao.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/frontend/src/utils/openAppFromKakao.ts b/frontend/src/utils/openAppFromKakao.ts index 3fa8c36f6..f9c419660 100644 --- a/frontend/src/utils/openAppFromKakao.ts +++ b/frontend/src/utils/openAppFromKakao.ts @@ -2,11 +2,12 @@ import { APP_STORE_LINKS, detectPlatform } from './appStoreLink'; const ANDROID_PACKAGE = 'com.moadong.moadong'; const APP_HOST = 'www.moadong.com'; +const IOS_SCHEME = 'moadongapp'; /** * 카카오톡 인앱 브라우저에서 앱을 실행하는 함수. * - Android: intent URL로 앱 직접 실행, 미설치 시 Play Store 이동 - * - iOS: Safari로 열어 Universal Link 트리거 (미설치 시 Smart App Banner 표시) + * - iOS: 커스텀 스킴으로 앱 실행 시도, 미설치 시 2초 후 App Store 이동 */ const openAppFromKakao = (path?: string) => { const platform = detectPlatform(); @@ -23,8 +24,21 @@ const openAppFromKakao = (path?: string) => { } const url = new URL(currentUrl); - const productionUrl = `https://${APP_HOST}${url.pathname}${url.search}${url.hash}`; - window.location.href = `kakaotalk://web/openExternal?url=${encodeURIComponent(productionUrl)}`; + window.location.href = `${IOS_SCHEME}://${url.pathname}${url.search}${url.hash}`; + + const timer = setTimeout(() => { + if (!document.hidden) { + window.location.href = `kakaotalk://web/openExternal?url=${encodeURIComponent(APP_STORE_LINKS.iphone)}`; + } + }, 2000); + + document.addEventListener( + 'visibilitychange', + () => { + if (document.hidden) clearTimeout(timer); + }, + { once: true }, + ); }; export default openAppFromKakao; From ce833f12ceb3f2da9106984c623464ad2c112526 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 20 Feb 2026 22:52:56 +0900 Subject: [PATCH 14/20] =?UTF-8?q?Revert=20"feat:=20iOS=20Safari=20Smart=20?= =?UTF-8?q?App=20Banner=20=EB=A9=94=ED=83=80=20=ED=83=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 12a427ebf87767c4588b7063ef839b2c443a1463. --- frontend/index.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 05aa8fd21..c8c0417ca 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -18,8 +18,6 @@ content="부경대학교 동아리 찾기, 모집 정보 확인부터 신규 동아리 가입과 홍보까지 한 번에 할 수 있어요." /> - - From 87471ce50e4ad6a729f8da703cd87d0f89ca371d Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 20 Feb 2026 23:37:57 +0900 Subject: [PATCH 15/20] =?UTF-8?q?refactor:=20=EC=95=B1=EC=97=B4=EA=B8=B0?= =?UTF-8?q?=20=EC=9C=A0=ED=8B=B8=EC=9D=84=20=ED=9B=85=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=ED=9B=84=20=EB=A1=9C=EB=94=A9=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/useOpenAppFromKakao.ts | 52 +++++++++++++++++++ .../ClubDetailTopBar/ClubDetailTopBar.tsx | 14 +++-- frontend/src/utils/openAppFromKakao.ts | 44 ---------------- 3 files changed, 62 insertions(+), 48 deletions(-) create mode 100644 frontend/src/hooks/useOpenAppFromKakao.ts delete mode 100644 frontend/src/utils/openAppFromKakao.ts diff --git a/frontend/src/hooks/useOpenAppFromKakao.ts b/frontend/src/hooks/useOpenAppFromKakao.ts new file mode 100644 index 000000000..e3441c909 --- /dev/null +++ b/frontend/src/hooks/useOpenAppFromKakao.ts @@ -0,0 +1,52 @@ +import { useState } from 'react'; +import { APP_STORE_LINKS, detectPlatform } from '@/utils/appStoreLink'; + +const ANDROID_PACKAGE = 'com.moadong.moadong'; +const APP_HOST = 'www.moadong.com'; +const IOS_SCHEME = 'moadongapp'; + +const useOpenAppFromKakao = () => { + const [isLoading, setIsLoading] = useState(false); + + const openApp = (path?: string) => { + const platform = detectPlatform(); + const currentUrl = path ?? window.location.href; + + if (platform === 'Android') { + const url = new URL(currentUrl); + const fallback = encodeURIComponent(APP_STORE_LINKS.android); + const intentUrl = + `intent://${APP_HOST}${url.pathname}${url.search}${url.hash}` + + `#Intent;scheme=https;package=${ANDROID_PACKAGE};S.browser_fallback_url=${fallback};end`; + window.location.href = intentUrl; + return; + } + + setIsLoading(true); + + const url = new URL(currentUrl); + window.location.href = `${IOS_SCHEME}://${url.pathname}${url.search}${url.hash}`; + + const timer = setTimeout(() => { + setIsLoading(false); + if (!document.hidden) { + window.location.href = `kakaotalk://web/openExternal?url=${encodeURIComponent(APP_STORE_LINKS.iphone)}`; + } + }, 2000); + + document.addEventListener( + 'visibilitychange', + () => { + if (document.hidden) { + clearTimeout(timer); + setIsLoading(false); + } + }, + { once: true }, + ); + }; + + return { openApp, isLoading }; +}; + +export default useOpenAppFromKakao; diff --git a/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx b/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx index 776e193b3..bece21592 100644 --- a/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx +++ b/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx @@ -3,10 +3,11 @@ import { useNavigate } from 'react-router-dom'; import { useTheme } from 'styled-components'; import NotificationIcon from '@/assets/images/icons/notification_icon.svg?react'; import PrevButtonIcon from '@/assets/images/icons/prev_button_icon.svg?react'; +import Spinner from '@/components/common/Spinner/Spinner'; import { useScrollTrigger } from '@/hooks/Scroll/useScrollTrigger'; +import useOpenAppFromKakao from '@/hooks/useOpenAppFromKakao'; import isInAppWebView from '@/utils/isInAppWebView'; import isKakaoTalkBrowser from '@/utils/isKakaoTalkBrowser'; -import openAppFromKakao from '@/utils/openAppFromKakao'; import { requestNavigateBack, requestNotificationSubscribe, @@ -46,6 +47,7 @@ const ClubDetailTopBar = ({ const theme = useTheme(); const isInApp = isInAppWebView(); const isKakao = !isInApp && isKakaoTalkBrowser(); + const { openApp, isLoading } = useOpenAppFromKakao(); const [isNotificationActive, setIsNotificationActive] = useState(initialIsSubscribed); @@ -122,9 +124,13 @@ const ClubDetailTopBar = ({ ) : isKakao ? ( - openAppFromKakao()}> - 앱열기 - + isLoading ? ( + + ) : ( + openApp()}> + 앱열기 + + ) ) : ( )} diff --git a/frontend/src/utils/openAppFromKakao.ts b/frontend/src/utils/openAppFromKakao.ts deleted file mode 100644 index f9c419660..000000000 --- a/frontend/src/utils/openAppFromKakao.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { APP_STORE_LINKS, detectPlatform } from './appStoreLink'; - -const ANDROID_PACKAGE = 'com.moadong.moadong'; -const APP_HOST = 'www.moadong.com'; -const IOS_SCHEME = 'moadongapp'; - -/** - * 카카오톡 인앱 브라우저에서 앱을 실행하는 함수. - * - Android: intent URL로 앱 직접 실행, 미설치 시 Play Store 이동 - * - iOS: 커스텀 스킴으로 앱 실행 시도, 미설치 시 2초 후 App Store 이동 - */ -const openAppFromKakao = (path?: string) => { - const platform = detectPlatform(); - const currentUrl = path ?? window.location.href; - - if (platform === 'Android') { - const url = new URL(currentUrl); - const fallback = encodeURIComponent(APP_STORE_LINKS.android); - const intentUrl = - `intent://${APP_HOST}${url.pathname}${url.search}${url.hash}` + - `#Intent;scheme=https;package=${ANDROID_PACKAGE};S.browser_fallback_url=${fallback};end`; - window.location.href = intentUrl; - return; - } - - const url = new URL(currentUrl); - window.location.href = `${IOS_SCHEME}://${url.pathname}${url.search}${url.hash}`; - - const timer = setTimeout(() => { - if (!document.hidden) { - window.location.href = `kakaotalk://web/openExternal?url=${encodeURIComponent(APP_STORE_LINKS.iphone)}`; - } - }, 2000); - - document.addEventListener( - 'visibilitychange', - () => { - if (document.hidden) clearTimeout(timer); - }, - { once: true }, - ); -}; - -export default openAppFromKakao; From 573a2c120a4128a6575c78f0a2fc17638629f908 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 20 Feb 2026 23:48:30 +0900 Subject: [PATCH 16/20] =?UTF-8?q?feat:=20=EC=8A=A4=ED=94=BC=EB=84=88=20?= =?UTF-8?q?=EC=98=A4=EB=B2=84=EB=A0=88=EC=9D=B4=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ClubDetailTopBar/ClubDetailTopBar.styles.ts | 11 +++++++++++ .../components/ClubDetailTopBar/ClubDetailTopBar.tsx | 11 +++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.styles.ts b/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.styles.ts index d36bf6fe7..9321e82a3 100644 --- a/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.styles.ts +++ b/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.styles.ts @@ -1,4 +1,5 @@ import styled from 'styled-components'; +import { Z_INDEX } from '@/styles/zIndex'; export const TopBarWrapper = styled.div<{ $isVisible: boolean }>` position: fixed; @@ -112,6 +113,16 @@ export const TabButton = styled.button<{ $active: boolean }>` transition: all 0.2s ease; `; +export const LoadingOverlay = styled.div` + position: fixed; + inset: 0; + z-index: ${Z_INDEX.overlay}; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(0, 0, 0, 0.3); +`; + export const AppOpenButton = styled.button` padding: 6px 12px; border: none; diff --git a/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx b/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx index bece21592..5893fe9f4 100644 --- a/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx +++ b/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx @@ -124,13 +124,16 @@ const ClubDetailTopBar = ({ ) : isKakao ? ( - isLoading ? ( - - ) : ( + <> + {isLoading && ( + + + + )} openApp()}> 앱열기 - ) + ) : ( )} From 73e5ef4ed3dc0d9430424ae2175b6af72d0efe1e Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 20 Feb 2026 23:57:49 +0900 Subject: [PATCH 17/20] =?UTF-8?q?fix:=20font-family=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ClubDetailTopBar/ClubDetailTopBar.styles.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.styles.ts b/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.styles.ts index 9321e82a3..3c2f82bc0 100644 --- a/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.styles.ts +++ b/frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.styles.ts @@ -133,7 +133,6 @@ export const AppOpenButton = styled.button` border-radius: 18px; cursor: pointer; white-space: nowrap; - font-family: 'Pretendard', sans-serif; transition: opacity 0.2s ease; &:active { From 6ff959a2cbdfba632149fa0b6c6f9d38569bee45 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Sat, 21 Feb 2026 00:14:52 +0900 Subject: [PATCH 18/20] =?UTF-8?q?refactor:=20path=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 상대경로 제외 --- frontend/src/hooks/useOpenAppFromKakao.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/hooks/useOpenAppFromKakao.ts b/frontend/src/hooks/useOpenAppFromKakao.ts index e3441c909..6cc7dc370 100644 --- a/frontend/src/hooks/useOpenAppFromKakao.ts +++ b/frontend/src/hooks/useOpenAppFromKakao.ts @@ -8,9 +8,9 @@ const IOS_SCHEME = 'moadongapp'; const useOpenAppFromKakao = () => { const [isLoading, setIsLoading] = useState(false); - const openApp = (path?: string) => { + const openApp = () => { const platform = detectPlatform(); - const currentUrl = path ?? window.location.href; + const currentUrl = window.location.href; if (platform === 'Android') { const url = new URL(currentUrl); From dac22b706daf1a9ded72c0425960de2ca7087eec Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Sat, 21 Feb 2026 00:34:09 +0900 Subject: [PATCH 19/20] =?UTF-8?q?fix:=20=ED=83=80=EC=9D=B4=EB=A8=B8=20?= =?UTF-8?q?=EB=88=84=EC=A0=81=20=EB=B0=8F=20=EC=96=B8=EB=A7=88=EC=9A=B4?= =?UTF-8?q?=ED=8A=B8=20=ED=9B=84=20=EB=A6=AC=EB=94=94=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useRef로 타이머를 추적해 중복 호출 시 이전 타이머 취소 - useEffect 정리 함수로 컴포넌트 언마운트 시 잔여 타이머 정리 --- frontend/src/hooks/useOpenAppFromKakao.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/frontend/src/hooks/useOpenAppFromKakao.ts b/frontend/src/hooks/useOpenAppFromKakao.ts index 6cc7dc370..a79449edb 100644 --- a/frontend/src/hooks/useOpenAppFromKakao.ts +++ b/frontend/src/hooks/useOpenAppFromKakao.ts @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { APP_STORE_LINKS, detectPlatform } from '@/utils/appStoreLink'; const ANDROID_PACKAGE = 'com.moadong.moadong'; @@ -7,6 +7,13 @@ const IOS_SCHEME = 'moadongapp'; const useOpenAppFromKakao = () => { const [isLoading, setIsLoading] = useState(false); + const timerRef = useRef | null>(null); + + useEffect(() => { + return () => { + if (timerRef.current !== null) clearTimeout(timerRef.current); + }; + }, []); const openApp = () => { const platform = detectPlatform(); @@ -22,12 +29,15 @@ const useOpenAppFromKakao = () => { return; } + if (timerRef.current !== null) clearTimeout(timerRef.current); + setIsLoading(true); const url = new URL(currentUrl); window.location.href = `${IOS_SCHEME}://${url.pathname}${url.search}${url.hash}`; - const timer = setTimeout(() => { + timerRef.current = setTimeout(() => { + timerRef.current = null; setIsLoading(false); if (!document.hidden) { window.location.href = `kakaotalk://web/openExternal?url=${encodeURIComponent(APP_STORE_LINKS.iphone)}`; @@ -37,8 +47,9 @@ const useOpenAppFromKakao = () => { document.addEventListener( 'visibilitychange', () => { - if (document.hidden) { - clearTimeout(timer); + if (document.hidden && timerRef.current !== null) { + clearTimeout(timerRef.current); + timerRef.current = null; setIsLoading(false); } }, From 9fa6472933aa9d8577a9a18db5b001637846646c Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Sat, 21 Feb 2026 14:20:48 +0900 Subject: [PATCH 20/20] =?UTF-8?q?docs:=202=EC=B4=88=20=ED=83=80=EC=9D=B4?= =?UTF-8?q?=EB=A8=B8=20=EC=9D=B4=EC=9C=A0=20=EC=A3=BC=EC=84=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- frontend/src/hooks/useOpenAppFromKakao.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/hooks/useOpenAppFromKakao.ts b/frontend/src/hooks/useOpenAppFromKakao.ts index a79449edb..687f4209e 100644 --- a/frontend/src/hooks/useOpenAppFromKakao.ts +++ b/frontend/src/hooks/useOpenAppFromKakao.ts @@ -43,6 +43,7 @@ const useOpenAppFromKakao = () => { window.location.href = `kakaotalk://web/openExternal?url=${encodeURIComponent(APP_STORE_LINKS.iphone)}`; } }, 2000); + // 2초 딜레이를 주는 이유는 앱 다운로드 페이지가 로드되는 시간을 주기 위함 document.addEventListener( 'visibilitychange',