Skip to content

Commit feb46aa

Browse files
Merge pull request #454 from FunD-StockProject/fix/qa-01-19
feat: 앱 설치 팝업 컴포넌트 추가 및 관련 스타일 정의
2 parents 41d3dc2 + 0e7ae4c commit feb46aa

9 files changed

Lines changed: 347 additions & 4 deletions

File tree

Lines changed: 11 additions & 0 deletions
Loading
Lines changed: 4 additions & 0 deletions
Loading
Lines changed: 4 additions & 0 deletions
Loading
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import styled from '@emotion/styled';
2+
import { theme } from '@styles/themes';
3+
4+
const Backdrop = styled('div')({
5+
position: 'fixed',
6+
top: 0,
7+
left: 0,
8+
width: '100%',
9+
height: '100%',
10+
background: 'rgba(0, 0, 0, 0.5)',
11+
zIndex: 999,
12+
display: 'flex',
13+
justifyContent: 'center',
14+
alignItems: 'center',
15+
});
16+
17+
const PopupContainer = styled.div({
18+
position: 'relative',
19+
display: 'flex',
20+
flexDirection: 'column',
21+
justifyContent: 'center',
22+
alignItems: 'center',
23+
padding: '24px 20px 16px',
24+
gap: '28px',
25+
width: '324px',
26+
background: theme.colors.sub_white,
27+
borderRadius: '20px',
28+
zIndex: 1000,
29+
boxSizing: 'border-box',
30+
});
31+
32+
const PopupTextContainer = styled.div({
33+
display: 'flex',
34+
flexDirection: 'column',
35+
alignItems: 'flex-start',
36+
padding: 0,
37+
gap: '12px',
38+
width: '284px',
39+
});
40+
41+
const TitleContainer = styled.div({
42+
display: 'flex',
43+
flexDirection: 'column',
44+
alignItems: 'flex-start',
45+
padding: 0,
46+
gap: '2px',
47+
width: '284px',
48+
});
49+
50+
const SubTitle = styled.p({
51+
margin: 0,
52+
width: '284px',
53+
fontFamily: 'Pretendard',
54+
...theme.font.body16Medium,
55+
color: theme.colors.sub_gray7,
56+
});
57+
58+
const Title = styled.h2({
59+
margin: 0,
60+
width: '284px',
61+
fontFamily: 'Pretendard',
62+
...theme.font.title20Semibold,
63+
color: theme.colors.sub_gray8,
64+
});
65+
66+
const FeaturesContainer = styled.div({
67+
display: 'flex',
68+
flexDirection: 'column',
69+
justifyContent: 'center',
70+
alignItems: 'center',
71+
padding: 0,
72+
gap: '10px',
73+
width: '284px',
74+
borderRadius: '5px',
75+
});
76+
77+
const FeatureItem = styled.div({
78+
display: 'flex',
79+
flexDirection: 'row',
80+
alignItems: 'center',
81+
padding: 0,
82+
gap: '10px',
83+
width: '284px',
84+
});
85+
86+
const IconWrapper = styled.div({
87+
display: 'flex',
88+
flexDirection: 'column',
89+
justifyContent: 'center',
90+
alignItems: 'center',
91+
padding: 0,
92+
gap: '10px',
93+
width: '32px',
94+
height: '32px',
95+
background: theme.colors.sub_blue5,
96+
borderRadius: '500px',
97+
flexShrink: 0,
98+
99+
['svg']: {
100+
width: '18px',
101+
height: '18px',
102+
fill: theme.colors.sub_gray1,
103+
},
104+
});
105+
106+
const FeatureTextContainer = styled.div({
107+
display: 'flex',
108+
flexDirection: 'column',
109+
justifyContent: 'center',
110+
alignItems: 'flex-start',
111+
padding: 0,
112+
flex: 1,
113+
});
114+
115+
const FeatureLabel = styled.p({
116+
margin: 0,
117+
fontFamily: 'Pretendard',
118+
...theme.font.detail12Medium,
119+
color: theme.colors.sub_gray7,
120+
});
121+
122+
const FeatureDescription = styled.p({
123+
margin: 0,
124+
fontFamily: 'Pretendard',
125+
...theme.font.body14Semibold,
126+
color: theme.colors.sub_gray10,
127+
});
128+
129+
const ButtonContainer = styled.div({
130+
display: 'flex',
131+
flexDirection: 'row',
132+
justifyContent: 'center',
133+
alignItems: 'center',
134+
padding: 0,
135+
gap: '12px',
136+
width: '284px',
137+
});
138+
139+
const CloseButton = styled.button({
140+
display: 'flex',
141+
flexDirection: 'row',
142+
justifyContent: 'center',
143+
alignItems: 'center',
144+
padding: '8px 0px',
145+
gap: '10px',
146+
width: '135px',
147+
height: '48px',
148+
background: theme.colors.sub_gray2,
149+
borderRadius: '500px',
150+
border: 'none',
151+
cursor: 'pointer',
152+
fontFamily: 'Pretendard',
153+
...theme.font.body18Semibold,
154+
textAlign: 'center',
155+
color: theme.colors.sub_gray8,
156+
outline: 'none',
157+
158+
['&:active']: {
159+
transform: 'scale(0.98)',
160+
},
161+
});
162+
163+
const DownloadButton = styled.button({
164+
display: 'flex',
165+
flexDirection: 'row',
166+
justifyContent: 'center',
167+
alignItems: 'center',
168+
padding: '8px 0px',
169+
gap: '10px',
170+
width: '137px',
171+
height: '48px',
172+
background: theme.colors.sub_blue6,
173+
borderRadius: '500px',
174+
border: 'none',
175+
cursor: 'pointer',
176+
fontFamily: 'Pretendard',
177+
...theme.font.body18Semibold,
178+
textAlign: 'center',
179+
color: theme.colors.grayscale10,
180+
outline: 'none',
181+
182+
['&:active']: {
183+
transform: 'scale(0.98)',
184+
},
185+
});
186+
187+
export {
188+
Backdrop,
189+
PopupContainer,
190+
PopupTextContainer,
191+
TitleContainer,
192+
SubTitle,
193+
Title,
194+
FeaturesContainer,
195+
FeatureItem,
196+
IconWrapper,
197+
FeatureTextContainer,
198+
FeatureLabel,
199+
FeatureDescription,
200+
ButtonContainer,
201+
CloseButton,
202+
DownloadButton,
203+
};
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { useState } from 'react';
2+
import useLocalStorageState from '@hooks/useLocalStorageState';
3+
import AlarmIcon from '@assets/appDownload/appDownloadAlarm.svg?react';
4+
import LightningIcon from '@assets/appDownload/appDownloadLightning.svg?react';
5+
import PhoneIcon from '@assets/appDownload/appDownloadPhone.svg?react';
6+
import {
7+
Backdrop,
8+
ButtonContainer,
9+
CloseButton,
10+
DownloadButton,
11+
FeatureDescription,
12+
FeatureItem,
13+
FeatureLabel,
14+
FeatureTextContainer,
15+
FeaturesContainer,
16+
PopupContainer,
17+
PopupTextContainer,
18+
SubTitle,
19+
Title,
20+
TitleContainer,
21+
} from './AppInstallPopUp.style';
22+
23+
interface AppInstallPopUpProps {
24+
onClose?: () => void;
25+
onDownload?: () => void;
26+
}
27+
28+
const AppInstallPopUp = ({ onClose, onDownload }: AppInstallPopUpProps) => {
29+
const [lastShown, setLastShown] = useLocalStorageState<string>('app_install_popup_last_shown');
30+
const [showPopUp, setShowPopUp] = useState(
31+
(() => {
32+
if (!lastShown) {
33+
return true;
34+
}
35+
36+
const diff = new Date().getTime() - new Date(lastShown).getTime();
37+
// 24시간 (1일) 지났으면 다시 보여주기
38+
if (diff < 1000 * 60 * 60 * 24) {
39+
return false;
40+
}
41+
42+
return true;
43+
})(),
44+
);
45+
46+
const handleClose = () => {
47+
setLastShown(new Date().toISOString());
48+
setShowPopUp(false);
49+
onClose?.();
50+
};
51+
52+
const handleDownload = () => {
53+
setShowPopUp(false);
54+
onDownload?.();
55+
// TODO: 실제 앱 다운로드 링크로 이동
56+
// 예: window.location.href = 'https://apps.apple.com/...' 또는 'https://play.google.com/...'
57+
};
58+
59+
const handleBackdropClick = () => {
60+
handleClose();
61+
};
62+
63+
if (!showPopUp) {
64+
return null;
65+
}
66+
67+
return (
68+
<Backdrop onClick={handleBackdropClick}>
69+
<PopupContainer onClick={(e) => e.stopPropagation()}>
70+
<PopupTextContainer>
71+
<TitleContainer>
72+
<SubTitle>앱에서 더 많은 기능을 경험해보세요!</SubTitle>
73+
<Title>인간지표 앱 출시!</Title>
74+
</TitleContainer>
75+
76+
<FeaturesContainer>
77+
<FeatureItem>
78+
<AlarmIcon />
79+
<FeatureTextContainer>
80+
<FeatureLabel>알림 기능으로</FeatureLabel>
81+
<FeatureDescription>시장 변화를 놓치지 않고!</FeatureDescription>
82+
</FeatureTextContainer>
83+
</FeatureItem>
84+
85+
<FeatureItem>
86+
<LightningIcon />
87+
<FeatureTextContainer>
88+
<FeatureLabel>더 쉽고 빠르게</FeatureLabel>
89+
<FeatureDescription>지표를 확인하고!</FeatureDescription>
90+
</FeatureTextContainer>
91+
</FeatureItem>
92+
93+
<FeatureItem>
94+
<PhoneIcon />
95+
<FeatureTextContainer>
96+
<FeatureLabel>앱 전용 기능까지</FeatureLabel>
97+
<FeatureDescription>숏뷰기능으로 더 유용하게!</FeatureDescription>
98+
</FeatureTextContainer>
99+
</FeatureItem>
100+
</FeaturesContainer>
101+
</PopupTextContainer>
102+
103+
<ButtonContainer>
104+
<CloseButton onClick={handleClose}>닫기</CloseButton>
105+
<DownloadButton onClick={handleDownload}>앱 다운받기</DownloadButton>
106+
</ButtonContainer>
107+
</PopupContainer>
108+
</Backdrop>
109+
);
110+
};
111+
112+
export default AppInstallPopUp;

src/hooks/useLocalStorageState.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ type LocalStorageKey =
88
| 'recent_stocks'
99
| 'tutorial_watched_shortview'
1010
| 'recent_provider'
11-
| 'last_visit_page';
11+
| 'last_visit_page'
12+
| 'app_install_popup_last_shown';
1213

1314
const useLocalStorageState = <T>(
1415
key: LocalStorageKey,

src/layout/Mainlayout/Mainlayout.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import { useLocation } from 'react-router-dom';
2-
import { detectPWA } from '@utils/Detector';
2+
import { detectPWA, detectPlatform, detectWebView } from '@utils/Detector';
33
import { webPath } from '@router/index';
44
import BottomNavigation from '@layout/BottomNavigation/BottomNavigation';
55
import Header from '@layout/Header/Header';
6+
import AppInstallPopUp from '@components/PopUp/AppInstallPopUp/AppInstallPopUp';
67
import PWAInfoPopUp from '@components/PopUp/PWAinfoPopUp/PWAInfoPopUp';
78
import Footer from '../Footer/Footer';
89
import { LayoutProps } from './Mainlayout.Props';
910
import { MainContent, StyledMainlayout } from './Mainlayout.Style';
1011

1112
const Mainlayout = ({ children }: LayoutProps) => {
1213
const location = useLocation();
14+
const platform = detectPlatform();
15+
const isMobileDevice = platform === 'iOS' || platform === 'Android';
1316
const visiblePWAInfoPopUp = false;
1417
const isRootPage = location.pathname === '/';
1518

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

3033
{visiblePWAInfoPopUp && isRootPage && !detectPWA() && <PWAInfoPopUp />}
34+
{isMobileDevice && isRootPage && !detectWebView() && <AppInstallPopUp />}
3135
{isBottomNavigationVisible && <BottomNavigation />}
3236
</StyledMainlayout>
3337
);

src/layout/SearchHeader/SearchHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { MESSAGE_TYPES } from '../../config/webview';
21
import { useNavigate } from 'react-router-dom';
32
import useAuthInfo from '@hooks/useAuthInfo';
43
import useLocalStorageState from '@hooks/useLocalStorageState';
@@ -18,6 +17,7 @@ import HeartIcon from '@assets/icons/heart.svg?react';
1817
import ToastBellSVG from '@assets/icons/toast/bell.svg?react';
1918
import ToastBellCrossSVG from '@assets/icons/toast/bell_cross.svg?react';
2019
import ToastHeartSVG from '@assets/icons/toast/heart.svg?react';
20+
import { MESSAGE_TYPES } from '../../config/webview';
2121
import { IconButton, RightSection, SearchHeaderWrapper } from './SearchHeader.Style';
2222

2323
const SearchHeader = ({ stockInfo }: { stockInfo: StockDetailInfo }) => {

src/utils/Detector.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,8 @@ const detectPWA = () => {
2323
return window.matchMedia('(display-mode: standalone)').matches;
2424
};
2525

26-
export { detectBrowser, detectPlatform, detectPWA };
26+
const detectWebView = () => {
27+
return !!(window as any).ReactNativeWebView;
28+
};
29+
30+
export { detectBrowser, detectPlatform, detectPWA, detectWebView };

0 commit comments

Comments
 (0)