Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions src/assets/appDownload/appDownloadAlarm.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/appDownload/appDownloadLightning.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/appDownload/appDownloadPhone.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
203 changes: 203 additions & 0 deletions src/components/PopUp/AppInstallPopUp/AppInstallPopUp.style.ts
Original file line number Diff line number Diff line change
@@ -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,
};
112 changes: 112 additions & 0 deletions src/components/PopUp/AppInstallPopUp/AppInstallPopUp.tsx
Original file line number Diff line number Diff line change
@@ -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<string>('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 (
<Backdrop onClick={handleBackdropClick}>
<PopupContainer onClick={(e) => e.stopPropagation()}>
<PopupTextContainer>
<TitleContainer>
<SubTitle>앱에서 더 많은 기능을 경험해보세요!</SubTitle>
<Title>인간지표 앱 출시!</Title>
</TitleContainer>

<FeaturesContainer>
<FeatureItem>
<AlarmIcon />
<FeatureTextContainer>
<FeatureLabel>알림 기능으로</FeatureLabel>
<FeatureDescription>시장 변화를 놓치지 않고!</FeatureDescription>
</FeatureTextContainer>
</FeatureItem>

<FeatureItem>
<LightningIcon />
<FeatureTextContainer>
<FeatureLabel>더 쉽고 빠르게</FeatureLabel>
<FeatureDescription>지표를 확인하고!</FeatureDescription>
</FeatureTextContainer>
</FeatureItem>

<FeatureItem>
<PhoneIcon />
<FeatureTextContainer>
<FeatureLabel>앱 전용 기능까지</FeatureLabel>
<FeatureDescription>숏뷰기능으로 더 유용하게!</FeatureDescription>
</FeatureTextContainer>
</FeatureItem>
</FeaturesContainer>
</PopupTextContainer>

<ButtonContainer>
<CloseButton onClick={handleClose}>닫기</CloseButton>
<DownloadButton onClick={handleDownload}>앱 다운받기</DownloadButton>
</ButtonContainer>
</PopupContainer>
</Backdrop>
);
};

export default AppInstallPopUp;
3 changes: 2 additions & 1 deletion src/hooks/useLocalStorageState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <T>(
key: LocalStorageKey,
Expand Down
6 changes: 5 additions & 1 deletion src/layout/Mainlayout/Mainlayout.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
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';
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 === '/';

Expand All @@ -28,6 +31,7 @@ const Mainlayout = ({ children }: LayoutProps) => {
</MainContent>

{visiblePWAInfoPopUp && isRootPage && !detectPWA() && <PWAInfoPopUp />}
{isMobileDevice && isRootPage && !detectWebView() && <AppInstallPopUp />}
{isBottomNavigationVisible && <BottomNavigation />}
</StyledMainlayout>
);
Expand Down
2 changes: 1 addition & 1 deletion src/layout/SearchHeader/SearchHeader.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 }) => {
Expand Down
6 changes: 5 additions & 1 deletion src/utils/Detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };