diff --git a/src/assets/appDownload/appDownloadAlarm.svg b/src/assets/appDownload/appDownloadAlarm.svg
new file mode 100644
index 0000000..896ca44
--- /dev/null
+++ b/src/assets/appDownload/appDownloadAlarm.svg
@@ -0,0 +1,11 @@
+
diff --git a/src/assets/appDownload/appDownloadLightning.svg b/src/assets/appDownload/appDownloadLightning.svg
new file mode 100644
index 0000000..1fe517d
--- /dev/null
+++ b/src/assets/appDownload/appDownloadLightning.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/appDownload/appDownloadPhone.svg b/src/assets/appDownload/appDownloadPhone.svg
new file mode 100644
index 0000000..4568840
--- /dev/null
+++ b/src/assets/appDownload/appDownloadPhone.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/components/PopUp/AppInstallPopUp/AppInstallPopUp.style.ts b/src/components/PopUp/AppInstallPopUp/AppInstallPopUp.style.ts
new file mode 100644
index 0000000..2df4313
--- /dev/null
+++ b/src/components/PopUp/AppInstallPopUp/AppInstallPopUp.style.ts
@@ -0,0 +1,203 @@
+import styled from '@emotion/styled';
+import { theme } from '@styles/themes';
+
+const Backdrop = styled('div')({
+ position: 'fixed',
+ top: 0,
+ left: 0,
+ width: '100%',
+ height: '100%',
+ background: 'rgba(0, 0, 0, 0.5)',
+ zIndex: 999,
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+});
+
+const PopupContainer = styled.div({
+ position: 'relative',
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: '24px 20px 16px',
+ gap: '28px',
+ width: '324px',
+ background: theme.colors.sub_white,
+ borderRadius: '20px',
+ zIndex: 1000,
+ boxSizing: 'border-box',
+});
+
+const PopupTextContainer = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'flex-start',
+ padding: 0,
+ gap: '12px',
+ width: '284px',
+});
+
+const TitleContainer = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'flex-start',
+ padding: 0,
+ gap: '2px',
+ width: '284px',
+});
+
+const SubTitle = styled.p({
+ margin: 0,
+ width: '284px',
+ fontFamily: 'Pretendard',
+ ...theme.font.body16Medium,
+ color: theme.colors.sub_gray7,
+});
+
+const Title = styled.h2({
+ margin: 0,
+ width: '284px',
+ fontFamily: 'Pretendard',
+ ...theme.font.title20Semibold,
+ color: theme.colors.sub_gray8,
+});
+
+const FeaturesContainer = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 0,
+ gap: '10px',
+ width: '284px',
+ borderRadius: '5px',
+});
+
+const FeatureItem = styled.div({
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 0,
+ gap: '10px',
+ width: '284px',
+});
+
+const IconWrapper = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 0,
+ gap: '10px',
+ width: '32px',
+ height: '32px',
+ background: theme.colors.sub_blue5,
+ borderRadius: '500px',
+ flexShrink: 0,
+
+ ['svg']: {
+ width: '18px',
+ height: '18px',
+ fill: theme.colors.sub_gray1,
+ },
+});
+
+const FeatureTextContainer = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'flex-start',
+ padding: 0,
+ flex: 1,
+});
+
+const FeatureLabel = styled.p({
+ margin: 0,
+ fontFamily: 'Pretendard',
+ ...theme.font.detail12Medium,
+ color: theme.colors.sub_gray7,
+});
+
+const FeatureDescription = styled.p({
+ margin: 0,
+ fontFamily: 'Pretendard',
+ ...theme.font.body14Semibold,
+ color: theme.colors.sub_gray10,
+});
+
+const ButtonContainer = styled.div({
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 0,
+ gap: '12px',
+ width: '284px',
+});
+
+const CloseButton = styled.button({
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: '8px 0px',
+ gap: '10px',
+ width: '135px',
+ height: '48px',
+ background: theme.colors.sub_gray2,
+ borderRadius: '500px',
+ border: 'none',
+ cursor: 'pointer',
+ fontFamily: 'Pretendard',
+ ...theme.font.body18Semibold,
+ textAlign: 'center',
+ color: theme.colors.sub_gray8,
+ outline: 'none',
+
+ ['&:active']: {
+ transform: 'scale(0.98)',
+ },
+});
+
+const DownloadButton = styled.button({
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: '8px 0px',
+ gap: '10px',
+ width: '137px',
+ height: '48px',
+ background: theme.colors.sub_blue6,
+ borderRadius: '500px',
+ border: 'none',
+ cursor: 'pointer',
+ fontFamily: 'Pretendard',
+ ...theme.font.body18Semibold,
+ textAlign: 'center',
+ color: theme.colors.grayscale10,
+ outline: 'none',
+
+ ['&:active']: {
+ transform: 'scale(0.98)',
+ },
+});
+
+export {
+ Backdrop,
+ PopupContainer,
+ PopupTextContainer,
+ TitleContainer,
+ SubTitle,
+ Title,
+ FeaturesContainer,
+ FeatureItem,
+ IconWrapper,
+ FeatureTextContainer,
+ FeatureLabel,
+ FeatureDescription,
+ ButtonContainer,
+ CloseButton,
+ DownloadButton,
+};
diff --git a/src/components/PopUp/AppInstallPopUp/AppInstallPopUp.tsx b/src/components/PopUp/AppInstallPopUp/AppInstallPopUp.tsx
new file mode 100644
index 0000000..243bb6d
--- /dev/null
+++ b/src/components/PopUp/AppInstallPopUp/AppInstallPopUp.tsx
@@ -0,0 +1,112 @@
+import { useState } from 'react';
+import useLocalStorageState from '@hooks/useLocalStorageState';
+import AlarmIcon from '@assets/appDownload/appDownloadAlarm.svg?react';
+import LightningIcon from '@assets/appDownload/appDownloadLightning.svg?react';
+import PhoneIcon from '@assets/appDownload/appDownloadPhone.svg?react';
+import {
+ Backdrop,
+ ButtonContainer,
+ CloseButton,
+ DownloadButton,
+ FeatureDescription,
+ FeatureItem,
+ FeatureLabel,
+ FeatureTextContainer,
+ FeaturesContainer,
+ PopupContainer,
+ PopupTextContainer,
+ SubTitle,
+ Title,
+ TitleContainer,
+} from './AppInstallPopUp.style';
+
+interface AppInstallPopUpProps {
+ onClose?: () => void;
+ onDownload?: () => void;
+}
+
+const AppInstallPopUp = ({ onClose, onDownload }: AppInstallPopUpProps) => {
+ const [lastShown, setLastShown] = useLocalStorageState('app_install_popup_last_shown');
+ const [showPopUp, setShowPopUp] = useState(
+ (() => {
+ if (!lastShown) {
+ return true;
+ }
+
+ const diff = new Date().getTime() - new Date(lastShown).getTime();
+ // 24시간 (1일) 지났으면 다시 보여주기
+ if (diff < 1000 * 60 * 60 * 24) {
+ return false;
+ }
+
+ return true;
+ })(),
+ );
+
+ const handleClose = () => {
+ setLastShown(new Date().toISOString());
+ setShowPopUp(false);
+ onClose?.();
+ };
+
+ const handleDownload = () => {
+ setShowPopUp(false);
+ onDownload?.();
+ // TODO: 실제 앱 다운로드 링크로 이동
+ // 예: window.location.href = 'https://apps.apple.com/...' 또는 'https://play.google.com/...'
+ };
+
+ const handleBackdropClick = () => {
+ handleClose();
+ };
+
+ if (!showPopUp) {
+ return null;
+ }
+
+ return (
+
+ e.stopPropagation()}>
+
+
+ 앱에서 더 많은 기능을 경험해보세요!
+ 인간지표 앱 출시!
+
+
+
+
+
+
+ 알림 기능으로
+ 시장 변화를 놓치지 않고!
+
+
+
+
+
+
+ 더 쉽고 빠르게
+ 지표를 확인하고!
+
+
+
+
+
+
+ 앱 전용 기능까지
+ 숏뷰기능으로 더 유용하게!
+
+
+
+
+
+
+ 닫기
+ 앱 다운받기
+
+
+
+ );
+};
+
+export default AppInstallPopUp;
diff --git a/src/hooks/useLocalStorageState.ts b/src/hooks/useLocalStorageState.ts
index 89d5378..26aece3 100644
--- a/src/hooks/useLocalStorageState.ts
+++ b/src/hooks/useLocalStorageState.ts
@@ -8,7 +8,8 @@ type LocalStorageKey =
| 'recent_stocks'
| 'tutorial_watched_shortview'
| 'recent_provider'
- | 'last_visit_page';
+ | 'last_visit_page'
+ | 'app_install_popup_last_shown';
const useLocalStorageState = (
key: LocalStorageKey,
diff --git a/src/layout/Mainlayout/Mainlayout.tsx b/src/layout/Mainlayout/Mainlayout.tsx
index f814981..cbe520f 100644
--- a/src/layout/Mainlayout/Mainlayout.tsx
+++ b/src/layout/Mainlayout/Mainlayout.tsx
@@ -1,8 +1,9 @@
import { useLocation } from 'react-router-dom';
-import { detectPWA } from '@utils/Detector';
+import { detectPWA, detectPlatform, detectWebView } from '@utils/Detector';
import { webPath } from '@router/index';
import BottomNavigation from '@layout/BottomNavigation/BottomNavigation';
import Header from '@layout/Header/Header';
+import AppInstallPopUp from '@components/PopUp/AppInstallPopUp/AppInstallPopUp';
import PWAInfoPopUp from '@components/PopUp/PWAinfoPopUp/PWAInfoPopUp';
import Footer from '../Footer/Footer';
import { LayoutProps } from './Mainlayout.Props';
@@ -10,6 +11,8 @@ import { MainContent, StyledMainlayout } from './Mainlayout.Style';
const Mainlayout = ({ children }: LayoutProps) => {
const location = useLocation();
+ const platform = detectPlatform();
+ const isMobileDevice = platform === 'iOS' || platform === 'Android';
const visiblePWAInfoPopUp = false;
const isRootPage = location.pathname === '/';
@@ -28,6 +31,7 @@ const Mainlayout = ({ children }: LayoutProps) => {
{visiblePWAInfoPopUp && isRootPage && !detectPWA() && }
+ {isMobileDevice && isRootPage && !detectWebView() && }
{isBottomNavigationVisible && }
);
diff --git a/src/layout/SearchHeader/SearchHeader.tsx b/src/layout/SearchHeader/SearchHeader.tsx
index 36de67c..84356b8 100644
--- a/src/layout/SearchHeader/SearchHeader.tsx
+++ b/src/layout/SearchHeader/SearchHeader.tsx
@@ -1,4 +1,3 @@
-import { MESSAGE_TYPES } from '../../config/webview';
import { useNavigate } from 'react-router-dom';
import useAuthInfo from '@hooks/useAuthInfo';
import useLocalStorageState from '@hooks/useLocalStorageState';
@@ -18,6 +17,7 @@ import HeartIcon from '@assets/icons/heart.svg?react';
import ToastBellSVG from '@assets/icons/toast/bell.svg?react';
import ToastBellCrossSVG from '@assets/icons/toast/bell_cross.svg?react';
import ToastHeartSVG from '@assets/icons/toast/heart.svg?react';
+import { MESSAGE_TYPES } from '../../config/webview';
import { IconButton, RightSection, SearchHeaderWrapper } from './SearchHeader.Style';
const SearchHeader = ({ stockInfo }: { stockInfo: StockDetailInfo }) => {
diff --git a/src/utils/Detector.ts b/src/utils/Detector.ts
index 79636a6..74f3995 100644
--- a/src/utils/Detector.ts
+++ b/src/utils/Detector.ts
@@ -23,4 +23,8 @@ const detectPWA = () => {
return window.matchMedia('(display-mode: standalone)').matches;
};
-export { detectBrowser, detectPlatform, detectPWA };
+const detectWebView = () => {
+ return !!(window as any).ReactNativeWebView;
+};
+
+export { detectBrowser, detectPlatform, detectPWA, detectWebView };