Skip to content

Commit 34692b5

Browse files
authored
Merge pull request #939 from Moadong/develop-fe
[release] FE v1.1.8
2 parents 3c658e4 + d14a323 commit 34692b5

File tree

10 files changed

+165
-46
lines changed

10 files changed

+165
-46
lines changed
2.58 MB
Loading
704 KB
Loading

frontend/src/components/application/QuestionTitle/QuestionTitle.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as Styled from './QuestionTitle.styles';
22
import { APPLICATION_FORM } from '@/constants/APPLICATION_FORM';
3-
import useIsMobile from '@/hooks/useIsMobile';
4-
import { useEffect, useLayoutEffect, useRef } from 'react';
3+
import useDevice from '@/hooks/useDevice';
4+
import { useLayoutEffect, useRef } from 'react';
55

66
interface QuestionTitleProps {
77
id: number;
@@ -18,7 +18,7 @@ const QuestionTitle = ({
1818
mode,
1919
onTitleChange,
2020
}: QuestionTitleProps) => {
21-
const isMobile = useIsMobile();
21+
const { isMobile } = useDevice();
2222
const textAreaRef = useRef<HTMLTextAreaElement>(null);
2323

2424
useLayoutEffect(() => {

frontend/src/constants/eventName.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ export const USER_EVENT = {
22
CATEGORY_BUTTON_CLICKED: 'CategoryButton Clicked',
33
SEARCH_BOX_CLICKED: 'SearchBox Clicked',
44

5+
// 배너 클릭
6+
BANNER_CLICKED: 'Banner Clicked',
7+
APP_DOWNLOAD_BANNER_CLICKED: 'App Download Banner Clicked',
8+
9+
510
// 네비게이션
611
BACK_BUTTON_CLICKED: 'Back Button Clicked',
712
HOME_BUTTON_CLICKED: 'Home Button Clicked',
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { renderHook, RenderHookResult } from '@testing-library/react';
2+
import { useNavigate } from 'react-router-dom';
3+
import useNavigator from '../useNavigator';
4+
5+
jest.mock('react-router-dom', () => ({
6+
useNavigate: jest.fn(),
7+
}));
8+
9+
describe('useNavigator - 사용자가 링크를 클릭했을 때', () => {
10+
const mockNavigate = jest.fn();
11+
const originalLocation = window.location;
12+
let handleLink: RenderHookResult<(url: string) => void, unknown>;
13+
14+
beforeEach(() => {
15+
jest.clearAllMocks();
16+
(useNavigate as jest.Mock).mockReturnValue(mockNavigate);
17+
18+
Object.defineProperty(window, 'location', {
19+
writable: true,
20+
value: { href: '' },
21+
});
22+
23+
// given
24+
handleLink = renderHook(() => useNavigator());
25+
});
26+
27+
afterEach(() => {
28+
Object.defineProperty(window, 'location', {
29+
writable: true,
30+
value: originalLocation,
31+
});
32+
});
33+
34+
describe('링크가 비어있으면', () => {
35+
it('아무 페이지로도 이동하지 않는다', () => {
36+
// When
37+
handleLink.result.current('');
38+
39+
// Then
40+
expect(mockNavigate).not.toHaveBeenCalled();
41+
expect(window.location.href).toBe('');
42+
});
43+
44+
it('공백만 있는 링크도 이동하지 않는다', () => {
45+
// When
46+
handleLink.result.current(' ');
47+
48+
// Then
49+
expect(mockNavigate).not.toHaveBeenCalled();
50+
expect(window.location.href).toBe('');
51+
});
52+
});
53+
54+
describe('악성 링크를 클릭하면', () => {
55+
it.each([
56+
['javascript', 'javascript:alert("XSS")'],
57+
['data', 'data:text/html,<script>alert("XSS")</script>'],
58+
['vbscript', 'vbscript:msgbox("XSS")'],
59+
['대문자 javascript', 'JAVASCRIPT:alert("XSS")'],
60+
])('%s 프로토콜 링크는 차단된다', (_, maliciousUrl) => {
61+
// When
62+
handleLink.result.current(maliciousUrl);
63+
64+
// Then
65+
expect(mockNavigate).not.toHaveBeenCalled();
66+
expect(window.location.href).toBe('');
67+
});
68+
});
69+
70+
describe('외부 사이트 링크를 클릭하면', () => {
71+
it.each([
72+
['https', 'https://example.com'],
73+
['http', 'http://example.com'],
74+
['App Store (itms-apps)', 'itms-apps://itunes.apple.com/app/123456'],
75+
])('%s 링크는 해당 사이트로 이동한다', (_, externalUrl) => {
76+
// When
77+
handleLink.result.current(externalUrl);
78+
79+
// Then
80+
expect(window.location.href).toBe(externalUrl);
81+
expect(mockNavigate).not.toHaveBeenCalled();
82+
});
83+
});
84+
85+
describe('내부 페이지 링크를 클릭하면', () => {
86+
it('소개 페이지로 이동할 수 있다', () => {
87+
// When
88+
handleLink.result.current('/introduce');
89+
90+
// Then
91+
expect(mockNavigate).toHaveBeenCalledWith('/introduce');
92+
expect(window.location.href).toBe('');
93+
});
94+
95+
it('상대 경로로도 이동할 수 있다', () => {
96+
// When
97+
handleLink.result.current('about');
98+
99+
// Then
100+
expect(mockNavigate).toHaveBeenCalledWith('about');
101+
expect(window.location.href).toBe('');
102+
});
103+
});
104+
});
105+

frontend/src/hooks/useIsMobile.ts

Lines changed: 0 additions & 18 deletions
This file was deleted.

frontend/src/hooks/useNavigator.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ const useNavigator = () => {
99
const trimmedUrl = url?.trim();
1010
if (!trimmedUrl) return;
1111

12-
const isExternalUrl = trimmedUrl.startsWith('https://');
12+
const isDangerousProtocol = /^(javascript|data|vbscript):/i.test(trimmedUrl);
13+
if (isDangerousProtocol) return;
14+
15+
const isExternalUrl = /^(https?|itms-apps):\/\//.test(trimmedUrl);
1316

1417
if (isExternalUrl) {
15-
window.open(trimmedUrl, '_blank', 'noopener,noreferrer');
18+
window.location.href = trimmedUrl;
1619
} else {
1720
navigate(trimmedUrl);
1821
}

frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ const tabs: TabCategory[] = [
4444
category: '계정 관리',
4545
items: [
4646
{ label: '비밀번호 수정', path: '/admin/account-edit' },
47-
{ label: '회원탈퇴', path: '/admin/user-delete' },
4847
],
4948
},
5049
];
@@ -64,14 +63,6 @@ const SideBar = ({ clubLogo, clubName }: SideBarProps) => {
6463
trackEvent(ADMIN_EVENT.TAB_CLICKED, {
6564
tabName: item.label,
6665
});
67-
// if (item.label === '아이디/비밀번호 수정') {
68-
// alert('아이디/비밀번호 수정 기능은 아직 준비 중이에요. ☺️');
69-
// return;
70-
// }
71-
if (item.label === '회원탈퇴') {
72-
alert('회원탈퇴 기능은 아직 준비 중이에요. ☺️');
73-
return;
74-
}
7566
navigate(item.path);
7667
};
7768

frontend/src/pages/MainPage/components/Banner/Banner.tsx

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,34 @@ import * as Styled from './Banner.styles';
66
import BANNERS from './bannerData';
77
import useDevice from '@/hooks/useDevice';
88
import useNavigator from '@/hooks/useNavigator';
9+
import useMixpanelTrack from '@/hooks/useMixpanelTrack';
10+
import { USER_EVENT } from '@/constants/eventName';
911
import PrevButton from '@/assets/images/icons/prev_button_icon.svg';
1012
import NextButton from '@/assets/images/icons/next_button_icon.svg';
1113

14+
15+
const APP_STORE_LINKS = {
16+
ios: 'itms-apps://itunes.apple.com/app/6755062085',
17+
android: 'https://play.google.com/store/apps/details?id=com.moadong.moadong&pcampaignid=web_share',
18+
default: 'https://play.google.com/store/apps/details?id=com.moadong.moadong&pcampaignid=web_share',
19+
};
20+
21+
const getAppStoreLink = (): string => {
22+
const userAgent = navigator.userAgent.toLowerCase();
23+
24+
if (/iphone|ipad|ipod|macintosh/.test(userAgent)) {
25+
return APP_STORE_LINKS.ios;
26+
}
27+
if (/android/.test(userAgent)) {
28+
return APP_STORE_LINKS.android;
29+
}
30+
return APP_STORE_LINKS.default;
31+
};
32+
1233
const Banner = () => {
1334
const { isMobile } = useDevice();
1435
const handleLink = useNavigator();
36+
const trackEvent = useMixpanelTrack();
1537
const [swiperInstance, setSwiperInstance] = useState<SwiperType | null>(null);
1638
const [currentIndex, setCurrentIndex] = useState(0);
1739

@@ -23,10 +45,22 @@ const Banner = () => {
2345
swiperInstance?.slideNext();
2446
};
2547

26-
const handleBannerClick = (url?: string) => {
27-
if (url) {
28-
handleLink(url);
48+
const handleBannerClick = (bannerId: string, bannerName: string, url?: string) => {
49+
if (!url) return;
50+
51+
if (url === 'APP_STORE_LINK') {
52+
const storeLink = getAppStoreLink();
53+
trackEvent(USER_EVENT.APP_DOWNLOAD_BANNER_CLICKED, {
54+
bannerId,
55+
bannerName,
56+
platform: /iphone|ipad|ipod|macintosh/.test(navigator.userAgent.toLowerCase()) ? 'ios' : 'android',
57+
});
58+
handleLink(storeLink);
59+
return;
2960
}
61+
62+
trackEvent(USER_EVENT.BANNER_CLICKED, { bannerId, bannerName, linkTo: url });
63+
handleLink(url);
3064
};
3165

3266
return (
@@ -56,7 +90,7 @@ const Banner = () => {
5690
<SwiperSlide key={banner.id}>
5791
<Styled.BannerItem
5892
isClickable={!!banner.linkTo}
59-
onClick={() => handleBannerClick(banner.linkTo)}
93+
onClick={() => handleBannerClick(banner.id, banner.alt, banner.linkTo)}
6094
>
6195
<img
6296
src={isMobile ? banner.mobileImage : banner.desktopImage}
Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import AllClubsDesktopImage from '@/assets/images/banners/banner_desktop1.png';
22
import StartNowDesktopImage from '@/assets/images/banners/banner_desktop2.png';
3-
import PatchNoteDesktopImage from '@/assets/images/banners/banner_desktop3.png';
43
import AllClubsMobileImage from '@/assets/images/banners/banner_mobile1.png';
54
import StartNowMobileImage from '@/assets/images/banners/banner_mobile2.png';
6-
import PatchNoteMobileImage from '@/assets/images/banners/banner_mobile3.png';
5+
import AppReleaseDesktopImage from '@/assets/images/banners/banner_desktop4.png';
6+
import AppReleaseMobileImage from '@/assets/images/banners/banner_mobile4.png';
77

88
interface BannerItem {
99
id: string;
@@ -14,6 +14,13 @@ interface BannerItem {
1414
}
1515

1616
const BANNERS: BannerItem[] = [
17+
{
18+
id: 'app-release-december-2025',
19+
desktopImage: AppReleaseDesktopImage,
20+
mobileImage: AppReleaseMobileImage,
21+
linkTo: 'APP_STORE_LINK',
22+
alt: '앱 다운로드 배너',
23+
},
1724
{
1825
id: 'all-clubs-in-one-place',
1926
desktopImage: AllClubsDesktopImage,
@@ -28,14 +35,6 @@ const BANNERS: BannerItem[] = [
2835
linkTo: '/introduce',
2936
alt: '지금 바로 모아동에서 시작하세요',
3037
},
31-
{
32-
id: 'patch-note-november-2025',
33-
desktopImage: PatchNoteDesktopImage,
34-
mobileImage: PatchNoteMobileImage,
35-
linkTo:
36-
'https://honorable-cough-8f9.notion.site/1e8aad232096804f9ea9ee4f5cf0cd10',
37-
alt: '모아동 11월 패치노트 안내 - 지원서 관리 및 메인페이지 개편',
38-
},
3938
];
4039

4140
export default BANNERS;

0 commit comments

Comments
 (0)