Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fbe9bd5
feat: 카카오톡 인앱 브라우저 감지 유틸 함수 추가
seongwon030 Feb 20, 2026
06c137c
feat: 카카오톡에서 앱 열기 유틸 함수 추가
seongwon030 Feb 20, 2026
1af18cf
feat: 카카오톡 인앱 브라우저에서 앱열기 버튼 표시
seongwon030 Feb 20, 2026
ea5c9ad
style: AppOpenButton pill 형태 스타일 추가
seongwon030 Feb 20, 2026
2c64d92
refactor: Android 앱 열기 방식을 외부 브라우저 기반으로 통일
seongwon030 Feb 20, 2026
d07370d
feat: Android 미설치 시 Play Store 폴백 추가
seongwon030 Feb 20, 2026
9d9d947
fix: Android intent URL host를 프로덕션 도메인으로 고정
seongwon030 Feb 20, 2026
dc1de10
fix: iOS 앱 열기 프로덕션 URL 고정 및 App Store 폴백 추가
seongwon030 Feb 20, 2026
14cac80
fix: iOS 미설치 시 App Store로 이동하도록 변경
seongwon030 Feb 20, 2026
1d10955
fix: 버튼 컬러 검은색으로 변경
seongwon030 Feb 20, 2026
288f821
fix: iOS 앱 열기 방식을 Universal Link 방식으로 복구
seongwon030 Feb 20, 2026
12a427e
feat: iOS Safari Smart App Banner 메타 태그 추가
seongwon030 Feb 20, 2026
8c2fb3c
feat: iOS 커스텀 스킴 기반 앱 실행 및 미설치 시 App Store 폴백 구현
seongwon030 Feb 20, 2026
ce833f1
Revert "feat: iOS Safari Smart App Banner 메타 태그 추가"
seongwon030 Feb 20, 2026
87471ce
refactor: 앱열기 유틸을 훅으로 변경 후 로딩상태 추가
seongwon030 Feb 20, 2026
573a2c1
feat: 스피너 오버레이 적용
seongwon030 Feb 20, 2026
73e5ef4
fix: font-family 제거
seongwon030 Feb 20, 2026
6ff959a
refactor: path 파라미터 제거
seongwon030 Feb 20, 2026
dac22b7
fix: 타이머 누적 및 언마운트 후 리디렉션 문제 수정
seongwon030 Feb 20, 2026
9fa6472
docs: 2초 타이머 이유 주석 추가
seongwon030 Feb 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions frontend/src/hooks/useOpenAppFromKakao.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useEffect, useRef, 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 timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

useEffect(() => {
return () => {
if (timerRef.current !== null) clearTimeout(timerRef.current);
};
}, []);

const openApp = () => {
const platform = detectPlatform();
const currentUrl = 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;
}

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}`;

timerRef.current = setTimeout(() => {
timerRef.current = null;
setIsLoading(false);
if (!document.hidden) {
window.location.href = `kakaotalk://web/openExternal?url=${encodeURIComponent(APP_STORE_LINKS.iphone)}`;
}
}, 2000);
// 2초 딜레이를 주는 이유는 앱 다운로드 페이지가 로드되는 시간을 주기 위함

document.addEventListener(
'visibilitychange',
() => {
if (document.hidden && timerRef.current !== null) {
clearTimeout(timerRef.current);
timerRef.current = null;
setIsLoading(false);
}
},
{ once: true },
);
};

return { openApp, isLoading };
};

export default useOpenAppFromKakao;
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import styled from 'styled-components';
import { Z_INDEX } from '@/styles/zIndex';

export const TopBarWrapper = styled.div<{ $isVisible: boolean }>`
position: fixed;
Expand Down Expand Up @@ -111,3 +112,30 @@ export const TabButton = styled.button<{ $active: boolean }>`
cursor: pointer;
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;
background-color: ${({ theme }) => theme.colors.base.black};
color: ${({ theme }) => theme.colors.base.white};
font-size: 13px;
font-weight: 600;
border-radius: 18px;
cursor: pointer;
white-space: nowrap;
transition: opacity 0.2s ease;

&:active {
opacity: 0.7;
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +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 {
requestNavigateBack,
requestNotificationSubscribe,
Expand Down Expand Up @@ -43,6 +46,8 @@ const ClubDetailTopBar = ({
const navigate = useNavigate();
const theme = useTheme();
const isInApp = isInAppWebView();
const isKakao = !isInApp && isKakaoTalkBrowser();
const { openApp, isLoading } = useOpenAppFromKakao();
const [isNotificationActive, setIsNotificationActive] =
useState(initialIsSubscribed);

Expand Down Expand Up @@ -118,6 +123,17 @@ const ClubDetailTopBar = ({
/>
</Styled.NotificationButton>
</Styled.IconButtonWrapper>
) : isKakao ? (
<>
{isLoading && (
<Styled.LoadingOverlay>
<Spinner height='auto' />
</Styled.LoadingOverlay>
)}
<Styled.AppOpenButton onClick={() => openApp()}>
앱열기
</Styled.AppOpenButton>
</>
) : (
<Styled.Placeholder />
)}
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/utils/isKakaoTalkBrowser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const isKakaoTalkBrowser = () => /KAKAOTALK/i.test(navigator.userAgent);

export default isKakaoTalkBrowser;
Loading