-
Notifications
You must be signed in to change notification settings - Fork 3
[release] 앱 출시 팝업 릴리즈 #1011
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
[release] 앱 출시 팝업 릴리즈 #1011
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
bc6c68e
feat: 플랫폼별 앱 스토어 링크 유틸 함수 추가
seongwon030 5e47045
feat: 팝업 배너 추가
seongwon030 4b57405
feat: 팝업 이벤트명 추가
seongwon030 86565b0
refactor: Banner 컴포넌트 앱 스토어 링크 로직 유틸로 분리
seongwon030 1001e4c
feat: 메인 페이지 앱 다운로드 팝업 컴포넌트 추가
seongwon030 e694694
feat: 메인 페이지 팝업 A/B 테스트 기능 추가
seongwon030 0662b7b
feat: 팝업 '다시 보지 않기' 7일 기한제 및 테스트 추가
seongwon030 655e5ed
style: 팝업모달 border radius 변경
seongwon030 20e203a
refactor: AB테스트 비율 상수로 분리
seongwon030 a3ac814
refactor: 팝업 닫기 이벤트 중복 트래킹 방지
seongwon030 c3fe6f1
feat: 팝업 이미지 preload로 렌더링 깜빡임 방지
seongwon030 e2347df
fix: 이미지 로드 실패 시 팝업 표시 안 되는 문제 해결
seongwon030 d4f5520
Merge pull request #1010 from Moadong/feature/#1009-app-pop-up-MOA-486
seongwon030 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
84 changes: 84 additions & 0 deletions
84
frontend/src/pages/MainPage/components/Popup/Popup.styles.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| import styled from 'styled-components'; | ||
| import { theme, Theme } from '@/styles/theme'; | ||
| import { Z_INDEX } from '@/styles/zIndex'; | ||
|
|
||
| export const Overlay = styled.div<{ isOpen: boolean }>` | ||
| inset: 0; | ||
| position: fixed; | ||
| z-index: ${Z_INDEX.overlay}; | ||
| background: rgba(0, 0, 0, ${({ isOpen }) => (isOpen ? 0.45 : 0)}); | ||
| display: grid; | ||
| place-items: center; | ||
| padding: 24px; | ||
| transition: background-color 0.2s ease; | ||
| `; | ||
|
|
||
| export const ModalContainer = styled.div<{ isOpen: boolean }>` | ||
| position: relative; | ||
| z-index: ${Z_INDEX.modal}; | ||
| max-width: 500px; | ||
| width: 100%; | ||
| max-height: 90vh; | ||
| background: transparent; | ||
| border-radius: 16px; | ||
| overflow: visible; | ||
| transition: transform 0.2s ease; | ||
| `; | ||
|
|
||
| export const Container = styled.div` | ||
| display: flex; | ||
| flex-direction: column; | ||
| width: 100%; | ||
| overflow: hidden; | ||
| background-color: #ffffff; | ||
| border-radius: 10px; | ||
| `; | ||
|
|
||
| export const ImageWrapper = styled.div` | ||
| width: 100%; | ||
| position: relative; | ||
| overflow: hidden; | ||
| cursor: pointer; | ||
|
|
||
| &:active { | ||
| opacity: 0.95; | ||
| } | ||
| `; | ||
|
|
||
| export const PopupImage = styled.img` | ||
| width: 100%; | ||
| height: auto; | ||
| display: block; | ||
| object-fit: cover; | ||
| `; | ||
|
|
||
| export const ButtonGroup = styled.div` | ||
| display: flex; | ||
| align-items: center; | ||
| width: 100%; | ||
| background-color: ${theme.colors.gray[900]}; | ||
| border-radius: 0 0 16px 16px; | ||
| `; | ||
|
|
||
| export const Button = styled.button` | ||
| flex: 1; | ||
| padding: 20px; | ||
| background-color: ${theme.colors.gray[900]}; | ||
| color: ${theme.colors.base.white}; | ||
| font-size: 15px; | ||
| font-weight: 500; | ||
| border: none; | ||
| cursor: pointer; | ||
| position: relative; | ||
|
|
||
| &:first-child::after { | ||
| content: ''; | ||
| position: absolute; | ||
| right: 0; | ||
| top: 50%; | ||
| transform: translateY(-50%); | ||
| width: 1px; | ||
| height: 40%; | ||
| background-color: ${theme.colors.gray[600]}; | ||
| } | ||
| `; | ||
156 changes: 156 additions & 0 deletions
156
frontend/src/pages/MainPage/components/Popup/Popup.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| import { | ||
| AB_TEST_KEY, | ||
| DAYS_TO_HIDE, | ||
| getABTestGroup, | ||
| isPopupHidden, | ||
| POPUP_STORAGE_KEY, | ||
| } from './Popup'; | ||
|
|
||
| describe('Popup 유틸 함수 테스트', () => { | ||
| beforeEach(() => { | ||
| localStorage.clear(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| localStorage.clear(); | ||
| }); | ||
|
|
||
| describe('getABTestGroup', () => { | ||
| it('저장된 그룹이 없으면 새로운 그룹을 할당한다', () => { | ||
| const group = getABTestGroup(); | ||
| expect(['show_popup', 'no_popup']).toContain(group); | ||
| expect(localStorage.getItem(AB_TEST_KEY)).toBe(group); | ||
| }); | ||
|
|
||
| it('저장된 그룹이 있으면 동일한 그룹을 반환한다', () => { | ||
| localStorage.setItem(AB_TEST_KEY, 'show_popup'); | ||
| const group = getABTestGroup(); | ||
| expect(group).toBe('show_popup'); | ||
| }); | ||
|
|
||
| it('여러 번 호출해도 동일한 그룹을 유지한다', () => { | ||
| const firstCall = getABTestGroup(); | ||
| const secondCall = getABTestGroup(); | ||
| const thirdCall = getABTestGroup(); | ||
|
|
||
| expect(firstCall).toBe(secondCall); | ||
| expect(secondCall).toBe(thirdCall); | ||
| }); | ||
|
|
||
| it('약 50/50 비율로 그룹을 할당한다', () => { | ||
| const results = { show_popup: 0, no_popup: 0 }; | ||
| const iterations = 1000; | ||
|
|
||
| for (let i = 0; i < iterations; i++) { | ||
| localStorage.clear(); | ||
| const group = getABTestGroup(); | ||
| results[group]++; | ||
| } | ||
|
|
||
| // 40-60% 범위 내에 있으면 통과 | ||
| expect(results.show_popup).toBeGreaterThan(iterations * 0.4); | ||
| expect(results.show_popup).toBeLessThan(iterations * 0.6); | ||
| }); | ||
| }); | ||
|
|
||
| describe('isPopupHidden', () => { | ||
| it('저장된 날짜가 없으면 false를 반환한다', () => { | ||
| expect(isPopupHidden()).toBe(false); | ||
| }); | ||
|
|
||
| it('7일 미만이면 true를 반환한다 (1일 전)', () => { | ||
| const oneDayAgo = Date.now() - 1 * 24 * 60 * 60 * 1000; | ||
| localStorage.setItem(POPUP_STORAGE_KEY, oneDayAgo.toString()); | ||
|
|
||
| expect(isPopupHidden()).toBe(true); | ||
| }); | ||
|
|
||
| it('7일 미만이면 true를 반환한다 (6일 전)', () => { | ||
| const sixDaysAgo = Date.now() - 6 * 24 * 60 * 60 * 1000; | ||
| localStorage.setItem(POPUP_STORAGE_KEY, sixDaysAgo.toString()); | ||
|
|
||
| expect(isPopupHidden()).toBe(true); | ||
| }); | ||
|
|
||
| it('정확히 7일이면 false를 반환한다', () => { | ||
| const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000; | ||
| localStorage.setItem(POPUP_STORAGE_KEY, sevenDaysAgo.toString()); | ||
|
|
||
| expect(isPopupHidden()).toBe(false); | ||
| }); | ||
|
|
||
| it('7일 이상이면 false를 반환한다 (8일 전)', () => { | ||
| const eightDaysAgo = Date.now() - 8 * 24 * 60 * 60 * 1000; | ||
| localStorage.setItem(POPUP_STORAGE_KEY, eightDaysAgo.toString()); | ||
|
|
||
| expect(isPopupHidden()).toBe(false); | ||
| }); | ||
|
|
||
| it('7일 이상이면 false를 반환한다 (30일 전)', () => { | ||
| const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000; | ||
| localStorage.setItem(POPUP_STORAGE_KEY, thirtyDaysAgo.toString()); | ||
|
|
||
| expect(isPopupHidden()).toBe(false); | ||
| }); | ||
|
|
||
| it('방금 저장한 경우 true를 반환한다', () => { | ||
| localStorage.setItem(POPUP_STORAGE_KEY, Date.now().toString()); | ||
|
|
||
| expect(isPopupHidden()).toBe(true); | ||
| }); | ||
|
|
||
| it('잘못된 형식의 날짜는 NaN으로 처리되어 false를 반환한다', () => { | ||
| localStorage.setItem(POPUP_STORAGE_KEY, 'invalid_date'); | ||
|
|
||
| expect(isPopupHidden()).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe('DAYS_TO_HIDE 상수', () => { | ||
| it('7일로 설정되어 있다', () => { | ||
| expect(DAYS_TO_HIDE).toBe(7); | ||
| }); | ||
| }); | ||
|
|
||
| describe('localStorage 키', () => { | ||
| it('POPUP_STORAGE_KEY가 올바르게 정의되어 있다', () => { | ||
| expect(POPUP_STORAGE_KEY).toBe('mainpage_popup_hidden_date'); | ||
| }); | ||
|
|
||
| it('AB_TEST_KEY가 올바르게 정의되어 있다', () => { | ||
| expect(AB_TEST_KEY).toBe('mainpage_popup_ab_group'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('통합 시나리오', () => { | ||
| it('시나리오: 첫 방문 → 다시 보지 않기 → 6일 후 방문 → 7일 후 방문', () => { | ||
| // 1. 첫 방문 | ||
| expect(isPopupHidden()).toBe(false); | ||
|
|
||
| // 2. "다시 보지 않기" 클릭 | ||
| localStorage.setItem(POPUP_STORAGE_KEY, Date.now().toString()); | ||
| expect(isPopupHidden()).toBe(true); | ||
|
|
||
| // 3. 6일 후 방문 (아직 숨김) | ||
| const sixDaysAgo = Date.now() - 6 * 24 * 60 * 60 * 1000; | ||
| localStorage.setItem(POPUP_STORAGE_KEY, sixDaysAgo.toString()); | ||
| expect(isPopupHidden()).toBe(true); | ||
|
|
||
| // 4. 7일 후 방문 (다시 표시) | ||
| const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000; | ||
| localStorage.setItem(POPUP_STORAGE_KEY, sevenDaysAgo.toString()); | ||
| expect(isPopupHidden()).toBe(false); | ||
| }); | ||
|
|
||
| it('시나리오: A/B 그룹 할당 후 팝업 숨김 상태 확인', () => { | ||
| const group = getABTestGroup(); | ||
| expect(['show_popup', 'no_popup']).toContain(group); | ||
|
|
||
| expect(isPopupHidden()).toBe(false); | ||
|
|
||
| localStorage.setItem(POPUP_STORAGE_KEY, Date.now().toString()); | ||
| expect(isPopupHidden()).toBe(true); | ||
| expect(getABTestGroup()).toBe(group); | ||
| }); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Container와ButtonGroup의 border-radius 불일치를 확인해 주세요.Container는border-radius: 10px를 사용하고,ButtonGroup은 하단 모서리에16px를 사용합니다.Container에overflow: hidden이 적용되어 있어ButtonGroup의 16px radius가 10px로 잘릴 수 있습니다.🔎 border-radius 통일 제안
Also applies to: 60-61
🤖 Prompt for AI Agents