[feature] 메인화면에 앱 출시 팝업을 추가한다#1010
Conversation
- Platform 타입 정의 ('iOS' | 'Android' | 'Other')
- getAppStoreLink: 사용자 플랫폼 감지 후 적절한 앱 스토어 링크 반환
- detectPlatform: iOS/Android/기타 플랫폼 감지
- APP_STORE_LINKS: 플랫폼별 앱 스토어 링크 상수
- 테스트 코드 작성 (12개 테스트, 100% 커버리지)
- APP_STORE_LINKS 상수 및 getAppStoreLink 함수 제거 - appStoreLink 유틸의 getAppStoreLink, detectPlatform 사용 - 플랫폼 감지 로직 간소화 및 코드 재사용성 향상
- 모바일에서만 표시되는 앱 출시 안내 팝업 구현 - useDevice 훅을 활용한 반응형 모바일 감지 - localStorage 기반 '다시 보지 않기' 기능 - 플랫폼별 앱 스토어 자동 연결 (iOS/Android) - Mixpanel 이벤트 트래킹 (팝업 표시, 닫기, 다운로드 클릭) - 이미지 클릭 시 앱 다운로드 페이지 이동 - 하단 버튼 그룹: 다시 보지 않기 / 닫기 - 배경 클릭으로 팝업 닫기 지원
- 사용자를 50/50 비율로 show_popup/no_popup 그룹에 랜덤 할당 - localStorage로 그룹 정보 영구 저장 (동일 사용자는 항상 같은 그룹) - show_popup 그룹만 팝업 표시, no_popup 그룹은 컨트롤 그룹 - 모든 Mixpanel 이벤트에 abTestGroup 속성 추가 - MAIN_POPUP_NOT_SHOWN 이벤트 추가 (컨트롤 그룹 트래킹) - PopupABTestGroup 타입 정의로 다른 A/B 테스트와 구분
- localStorage에 타임스탬프 저장하여 7일 후 자동 재표시 - isPopupHidden 함수로 날짜 기반 팝업 숨김 로직 구현 - POPUP_STORAGE_KEY를 'mainpage_popup_hidden_date'로 변경 - 테스트를 위해 상수 및 유틸 함수 export - 17개 테스트 케이스 작성 (모두 통과) - A/B 테스트 그룹 할당 테스트 - 7일 기반 팝업 숨김 로직 테스트 - 통합 시나리오 테스트 - 핵심 로직 90.9% 커버리지 달성
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded@seongwon030 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 3 minutes and 26 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (1)
Warning
|
| Cohort / File(s) | 요약 |
|---|---|
이벤트 상수 frontend/src/constants/eventName.ts |
USER_EVENT에 팝업 관련 4개 키 추가: MAIN_POPUP_VIEWED, MAIN_POPUP_NOT_SHOWN, MAIN_POPUP_CLOSED, APP_DOWNLOAD_POPUP_CLICKED |
메인 페이지 frontend/src/pages/MainPage/MainPage.tsx |
Popup 컴포넌트 임포트 및 렌더링 추가; 불필요 TODO 주석 제거 |
배너 리팩토링 frontend/src/pages/MainPage/components/Banner/Banner.tsx |
인라인 App Store 링크/플랫폼 로직 제거, frontend/src/utils/appStoreLink의 detectPlatform/getAppStoreLink 사용으로 교체 |
Popup 컴포넌트 코드 frontend/src/pages/MainPage/components/Popup/Popup.tsx |
신규 Popup 컴포넌트 추가: AB 테스트(키, 그룹), DAYS_TO_HIDE 기반 숨김 로직, Mixpanel 이벤트(뷰/미표시/닫기/다운로드) 및 다운로드 흐름 구현; 로컬스토리지·이미지 프리로드·바디 스크롤 잠금 처리 포함 |
Popup 스타일 frontend/src/pages/MainPage/components/Popup/Popup.styles.ts |
overlay/modal/button 등 styled-components로 UI 스타일 추가 (isOpen props 포함) |
Popup 테스트 frontend/src/pages/MainPage/components/Popup/Popup.test.tsx |
AB 그룹 분배, 7일 숨김 로직, 통합 시나리오 등 광범위한 단위 테스트 추가 |
App Store 유틸리티 frontend/src/utils/appStoreLink.ts, frontend/src/utils/appStoreLink.test.ts |
Platform 타입, APP_STORE_LINKS 상수, detectPlatform() 및 getAppStoreLink() 도입 및 해당 유닛 테스트 추가 |
Sequence Diagram(s)
sequenceDiagram
autonumber
participant User
participant MainPage
participant Popup
participant Detector as PlatformDetector
participant Storage as localStorage
participant Mixpanel
participant AppStore
User->>MainPage: 메인페이지 접속
MainPage->>Popup: 마운트/초기 렌더
Popup->>Detector: isMobile() / detectPlatform()
Detector-->>Popup: 모바일 여부 / 플랫폼
alt 모바일이고 숨김 아님
Popup->>Storage: POPUP_STORAGE_KEY 확인
Storage-->>Popup: 숨김 날짜 or 없음
Popup->>Storage: AB_TEST_KEY 확인/생성
Storage-->>Popup: AB 그룹 (show_popup / no_popup)
alt show_popup
Popup->>Mixpanel: MAIN_POPUP_VIEWED
Popup->>User: 팝업 표시
User->>Popup: 이미지/다운로드 클릭
Popup->>AppStore: getAppStoreLink() 열기
Popup->>Mixpanel: APP_DOWNLOAD_POPUP_CLICKED
Popup->>Storage: 숨김 날짜 저장 (또는 사용자가 저장)
User->>Popup: 다시 보지 않기 클릭
Popup->>Storage: 숨김 날짜 저장
Popup->>Mixpanel: MAIN_POPUP_CLOSED
User->>Popup: 배경/닫기 클릭
Popup->>Mixpanel: MAIN_POPUP_CLOSED (backdrop/close)
else no_popup
Popup->>Mixpanel: MAIN_POPUP_NOT_SHOWN
end
else 모바일 아님 또는 숨김 기간 내
Popup->>Mixpanel: MAIN_POPUP_NOT_SHOWN
end
Estimated code review effort
🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related issues
- MOA-486: 앱 출시 팝업을 추가한다: 메인페이지 진입 시 앱 출시 팝업을 띄우는 요구사항과 직접 일치합니다.
- [feature] MOA-486 메인화면에 앱 출시 팝업을 추가한다 #1009: 메인페이지 팝업 및 AB 테스트 구현을 목표로 하는 이슈로 기능적으로 연관됩니다.
Possibly related PRs
- [feature] 관리자페이지 믹스패널 로깅을 적용한다 #874:
eventName.ts관련 변경 — 본 PR의 USER_EVENT 추가와 동일한 상수 영역을 수정합니다. - [feature] 앱 출시 배너를 추가한다 #938: 배너의 앱스토어 링크·플랫폼 감지 로직 관련 — 본 PR이 해당 로직을
appStoreLink유틸로 추출·대체했습니다. - [release] FE v1.1.8 #939: 앱스토어 링크·배너/추적 흐름 관련 변경과 코드 수준 연관이 있습니다.
Suggested reviewers
- lepitaaar
- oesnuj
- suhyun113
Pre-merge checks and finishing touches
✅ Passed checks (5 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title check | ✅ Passed | PR 제목은 메인화면에 앱 출시 팝업을 추가하는 변경사항을 명확하게 설명하며, 실제 코드 변경 내용과 일치합니다. |
| Linked Issues check | ✅ Passed | PR은 MOA-486에서 요구한 메인페이지 진입 시 앱 출시 팝업 표시, A/B 테스트, 7일 숨김 기능, 플랫폼 감지, 이벤트 추적 등 모든 핵심 요구사항을 충족합니다. |
| Out of Scope Changes check | ✅ Passed | 모든 코드 변경은 앱 출시 팝업 구현과 관련된 범위 내에 있으며, 불필요한 변경은 발견되지 않았습니다. |
| Docstring Coverage | ✅ Passed | No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. |
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands and usage tips.
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Fix all issues with AI Agents
In @frontend/src/pages/MainPage/components/Popup/Popup.styles.ts:
- Around line 23-60: ModalContainer and Container use a 10px border-radius while
ButtonGroup uses 16px, causing visual inconsistency; update ButtonGroup (or
introduce a shared constant) so its bottom radii match the modal’s 10px (e.g.,
change border-radius on ButtonGroup from 0 0 16px 16px to 0 0 10px 10px or
replace hardcoded values with a shared BORDER_RADIUS used by ModalContainer,
Container, and ButtonGroup) to ensure the modal’s corners align visually.
In @frontend/src/pages/MainPage/components/Popup/Popup.tsx:
- Line 13: The type PopupABTestGroup is declared but not exported, causing tests
that reference it to fail; export the type by adding an export modifier to the
PopupABTestGroup declaration so tests can import it (i.e., change the
declaration of PopupABTestGroup to be exported) and update any local imports if
needed.
- Around line 104-114: handleBackdropClick currently tracks a "backdrop_click"
event and then calls handleClose(), which itself also tracks a close event
(resulting in duplicate/incorrect "close_button" events); update the logic so
only one close event is emitted: modify handleClose to accept an optional
parameter (e.g., reason or skipTracking flag) or an action string (default
'close_button'), then have handleBackdropClick call
handleClose('backdrop_click') or handleClose({ skipTracking: true }) and ensure
handleClose uses that parameter to either emit the correct tracked action
('backdrop_click') or skip its internal tracking when skipTracking is true;
reference functions: handleBackdropClick and handleClose.
- Around line 22-23: Extract the magic number 0.5 into a clearly named constant
(e.g., POPUP_AB_TEST_RATIO or AB_TEST_SPLIT_RATIO) and use that constant when
computing the PopupABTestGroup; replace the inline Math.random() < 0.5
expression in Popup.tsx with Math.random() < POPUP_AB_TEST_RATIO and ensure the
new constant is declared near the top of the module (exported if used elsewhere)
so the 50/50 split is explicit and maintainable.
In @frontend/src/utils/appStoreLink.ts:
- Line 26: In getAppStoreLink (frontend/src/utils/appStoreLink.ts) the userAgent
check currently tests for "macintosh" which misclassifies macOS desktop as iOS;
remove "macintosh" from the regex (leave only iphone|ipad|ipod) or replace the
test with an explicit iOS-only detection using the userAgent variable so desktop
macs are not treated as iOS devices.
- Line 14: The current user-agent check incorrectly treats desktop macOS as iOS
because it includes "macintosh" in if
(/iphone|ipad|ipod|macintosh/.test(userAgent)); change this to only match real
iOS identifiers (e.g. replace with /iphone|ipad|ipod/i.test(userAgent)), or if
you need to catch iPadOS that may present as "Macintosh" use a safer check like
((/iphone|ipad|ipod/i.test(userAgent)) || (/macintosh/i.test(userAgent) &&
/mobile/i.test(userAgent))) referencing the same userAgent variable in
frontend/src/utils/appStoreLink.ts.
🧹 Nitpick comments (8)
frontend/src/pages/MainPage/components/Popup/Popup.styles.ts (3)
5-26: 매직 넘버를 명명된 상수로 추출하세요.Overlay와 ModalContainer에 하드코딩된 값들(0.45, 24px, 500px, 90vh, 10px 등)이 있습니다. 코딩 가이드라인에 따라 이러한 매직 넘버를 명명된 상수로 추출하여 의미를 명확히 하고 유지보수성을 향상시키는 것이 좋습니다.
🔎 제안하는 리팩터링
+const MODAL_CONFIG = { + OVERLAY_OPACITY: 0.45, + OVERLAY_PADDING: '24px', + MAX_WIDTH: '500px', + MAX_HEIGHT: '90vh', + BORDER_RADIUS: '10px', + TRANSITION_DURATION: '0.2s', +} as const; + 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)}); + background: rgba(0, 0, 0, ${({ isOpen }) => (isOpen ? MODAL_CONFIG.OVERLAY_OPACITY : 0)}); display: grid; place-items: center; - padding: 24px; + padding: ${MODAL_CONFIG.OVERLAY_PADDING}; - transition: background-color 0.2s ease; + transition: background-color ${MODAL_CONFIG.TRANSITION_DURATION} ease; `; export const ModalContainer = styled.div<{ isOpen: boolean }>` position: relative; z-index: ${Z_INDEX.modal}; - max-width: 500px; + max-width: ${MODAL_CONFIG.MAX_WIDTH}; width: 100%; - max-height: 90vh; + max-height: ${MODAL_CONFIG.MAX_HEIGHT}; background: transparent; - border-radius: 10px; + border-radius: ${MODAL_CONFIG.BORDER_RADIUS}; overflow: visible; - transition: transform 0.2s ease; + transition: transform ${MODAL_CONFIG.TRANSITION_DURATION} ease; `;
33-33: 테마 색상을 사용하세요.하드코딩된
#ffffff대신theme.colors.base.white를 사용하여 일관성을 유지하세요.🔎 제안하는 수정
export const Container = styled.div` display: flex; flex-direction: column; width: 100%; overflow: hidden; - background-color: #ffffff; + background-color: ${theme.colors.base.white}; border-radius: 10px; `;
63-84: Button 컴포넌트의 매직 넘버를 정리하세요.버튼 관련 스타일 값들(20px, 15px, 500, 1px, 40%)도 명명된 상수로 추출하면 유지보수가 용이합니다.
🔎 제안하는 리팩터링
+const BUTTON_CONFIG = { + PADDING: '20px', + FONT_SIZE: '15px', + FONT_WEIGHT: 500, + DIVIDER_WIDTH: '1px', + DIVIDER_HEIGHT: '40%', +} as const; + export const Button = styled.button` flex: 1; - padding: 20px; + padding: ${BUTTON_CONFIG.PADDING}; background-color: ${theme.colors.gray[900]}; color: ${theme.colors.base.white}; - font-size: 15px; + font-size: ${BUTTON_CONFIG.FONT_SIZE}; - font-weight: 500; + font-weight: ${BUTTON_CONFIG.FONT_WEIGHT}; border: none; cursor: pointer; position: relative; &:first-child::after { content: ''; position: absolute; right: 0; top: 50%; transform: translateY(-50%); - width: 1px; + width: ${BUTTON_CONFIG.DIVIDER_WIDTH}; - height: 40%; + height: ${BUTTON_CONFIG.DIVIDER_HEIGHT}; background-color: ${theme.colors.gray[600]}; } `;frontend/src/utils/appStoreLink.ts (1)
11-33: 코드 중복을 제거하여 유지보수성을 개선하세요.
getAppStoreLink와detectPlatform함수가 동일한 userAgent 감지 로직을 중복해서 사용하고 있습니다. 한 함수가 다른 함수를 호출하도록 리팩터링하면 일관성을 유지하기 쉬워집니다.🔎 제안된 리팩터링
export const getAppStoreLink = (): string => { - const userAgent = navigator.userAgent.toLowerCase(); - - if (/iphone|ipad|ipod|macintosh/.test(userAgent)) { - return APP_STORE_LINKS.ios; - } - if (/android/.test(userAgent)) { - return APP_STORE_LINKS.android; - } - return APP_STORE_LINKS.default; + const platform = detectPlatform(); + + switch (platform) { + case 'iOS': + return APP_STORE_LINKS.ios; + case 'Android': + return APP_STORE_LINKS.android; + default: + return APP_STORE_LINKS.default; + } };frontend/src/pages/MainPage/components/Popup/Popup.tsx (4)
15-27: 함수에 부수 효과가 있습니다.
getABTestGroup함수는 읽기 작업처럼 보이지만 localStorage에 쓰기를 수행하는 부수 효과가 있습니다. 코딩 가이드라인에 따르면 함수는 시그니처가 암시하는 동작만 수행해야 합니다 (단일 책임 원칙).함수명을
ensureABTestGroup또는getOrInitializeABTestGroup으로 변경하거나, 읽기와 쓰기 로직을 분리하는 것을 고려하세요.
38-143: AB 테스트 그룹을 한 번만 계산하여 성능을 개선하세요.컴포넌트 내에서
getABTestGroup()이 6번 호출됩니다 (lines 45, 73, 83, 95, 106). 각 호출마다 localStorage를 읽습니다. 한 번만 계산하여 state나 ref에 저장하는 것이 더 효율적입니다.🔎 제안된 리팩터링
const Popup = () => { const [isOpen, setIsOpen] = useState(false); const { isMobile } = useDevice(); const trackEvent = useMixpanelTrack(); + const [abGroup] = useState(() => getABTestGroup()); useEffect(() => { const isHidden = isPopupHidden(); - const abGroup = getABTestGroup(); if (isMobile && !isHidden) { if (abGroup === 'show_popup') { setIsOpen(true); trackEvent(USER_EVENT.MAIN_POPUP_VIEWED, { popupType: 'app_download', abTestGroup: abGroup, }); } else { trackEvent(USER_EVENT.MAIN_POPUP_NOT_SHOWN, { popupType: 'app_download', abTestGroup: abGroup, }); } } - }, [isMobile, trackEvent]); + }, [isMobile, trackEvent, abGroup]); // ... 이후 모든 getABTestGroup() 호출을 abGroup 변수로 대체
43-61: useEffect가 여러 책임을 가지고 있습니다.이 useEffect는 다음을 모두 수행합니다:
- 팝업이 숨겨져 있는지 확인
- AB 테스트 그룹 가져오기
- 팝업 열기 결정
- 이벤트 추적
코딩 가이드라인에 따르면 복잡한 로직은 더 작고 집중된 훅/컨텍스트로 분리해야 합니다. 이 로직을 커스텀 훅 (예:
usePopupVisibility)으로 추출하는 것을 고려하세요.
101-101: 팝업 차단 여부를 확인하세요.
window.open()은 팝업 차단기에 의해 차단될 수 있습니다. 차단된 경우 사용자에게 피드백을 제공하거나 에러를 처리하는 것을 고려하세요.🔎 제안된 개선
- window.open(storeLink, '_blank'); + const newWindow = window.open(storeLink, '_blank'); + if (!newWindow) { + // 팝업이 차단된 경우 처리 + console.warn('Popup blocked. Please allow popups for this site.'); + // 또는 사용자에게 알림 표시 + }
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
frontend/src/assets/images/popup/app-download.svgis excluded by!**/*.svg
📒 Files selected for processing (8)
frontend/src/constants/eventName.tsfrontend/src/pages/MainPage/MainPage.tsxfrontend/src/pages/MainPage/components/Banner/Banner.tsxfrontend/src/pages/MainPage/components/Popup/Popup.styles.tsfrontend/src/pages/MainPage/components/Popup/Popup.test.tsxfrontend/src/pages/MainPage/components/Popup/Popup.tsxfrontend/src/utils/appStoreLink.test.tsfrontend/src/utils/appStoreLink.ts
🧰 Additional context used
📓 Path-based instructions (3)
frontend/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.{ts,tsx,js,jsx}: Replace magic numbers with named constants for clarity
Replace complex/nested ternaries withif/elseor IIFEs for readability
Assign complex boolean conditions to named variables for explicit meaning
Avoid hidden side effects; functions should only perform actions implied by their signature (Single Responsibility Principle)
Use unique and descriptive names for custom wrappers/functions to avoid ambiguity
Define constants near related logic or ensure names link them clearly to avoid silent failures
Break down broad state management into smaller, focused hooks/contexts to reduce coupling
Files:
frontend/src/constants/eventName.tsfrontend/src/utils/appStoreLink.test.tsfrontend/src/utils/appStoreLink.tsfrontend/src/pages/MainPage/components/Popup/Popup.test.tsxfrontend/src/pages/MainPage/MainPage.tsxfrontend/src/pages/MainPage/components/Popup/Popup.tsxfrontend/src/pages/MainPage/components/Banner/Banner.tsxfrontend/src/pages/MainPage/components/Popup/Popup.styles.ts
frontend/**/*.{ts,tsx}
📄 CodeRabbit inference engine (frontend/.cursorrules)
Use consistent return types for similar functions/hooks
Files:
frontend/src/constants/eventName.tsfrontend/src/utils/appStoreLink.test.tsfrontend/src/utils/appStoreLink.tsfrontend/src/pages/MainPage/components/Popup/Popup.test.tsxfrontend/src/pages/MainPage/MainPage.tsxfrontend/src/pages/MainPage/components/Popup/Popup.tsxfrontend/src/pages/MainPage/components/Banner/Banner.tsxfrontend/src/pages/MainPage/components/Popup/Popup.styles.ts
frontend/**/*.{tsx,jsx}
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.{tsx,jsx}: Abstract complex logic/interactions into dedicated components/HOCs
Separate significantly different conditional UI/logic into distinct components
Colocate simple, localized logic or use inline definitions to reduce context switching
Choose field-level or form-level cohesion based on form requirements when using form libraries like react-hook-form
Use Component Composition instead of Props Drilling to reduce coupling
Files:
frontend/src/pages/MainPage/components/Popup/Popup.test.tsxfrontend/src/pages/MainPage/MainPage.tsxfrontend/src/pages/MainPage/components/Popup/Popup.tsxfrontend/src/pages/MainPage/components/Banner/Banner.tsx
🧠 Learnings (4)
📚 Learning: 2025-11-25T14:08:23.253Z
Learnt from: CR
Repo: Moadong/moadong PR: 0
File: frontend/.cursorrules:0-0
Timestamp: 2025-11-25T14:08:23.253Z
Learning: Applies to frontend/**/*.{ts,tsx,js,jsx} : Define constants near related logic or ensure names link them clearly to avoid silent failures
Applied to files:
frontend/src/utils/appStoreLink.ts
📚 Learning: 2025-11-25T14:08:23.253Z
Learnt from: CR
Repo: Moadong/moadong PR: 0
File: frontend/.cursorrules:0-0
Timestamp: 2025-11-25T14:08:23.253Z
Learning: Applies to frontend/**/*.{ts,tsx,js,jsx} : Replace magic numbers with named constants for clarity
Applied to files:
frontend/src/utils/appStoreLink.ts
📚 Learning: 2025-03-19T05:18:07.818Z
Learnt from: seongwon030
Repo: Moadong/moadong PR: 195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.
Applied to files:
frontend/src/pages/MainPage/MainPage.tsx
📚 Learning: 2025-11-25T14:08:23.253Z
Learnt from: CR
Repo: Moadong/moadong PR: 0
File: frontend/.cursorrules:0-0
Timestamp: 2025-11-25T14:08:23.253Z
Learning: Applies to frontend/**/*.{tsx,jsx} : Abstract complex logic/interactions into dedicated components/HOCs
Applied to files:
frontend/src/pages/MainPage/components/Popup/Popup.styles.ts
🧬 Code graph analysis (4)
frontend/src/utils/appStoreLink.test.ts (1)
frontend/src/utils/appStoreLink.ts (3)
detectPlatform(23-33)getAppStoreLink(11-21)APP_STORE_LINKS(1-7)
frontend/src/pages/MainPage/components/Popup/Popup.tsx (2)
frontend/src/constants/eventName.ts (1)
USER_EVENT(1-44)frontend/src/utils/appStoreLink.ts (2)
getAppStoreLink(11-21)detectPlatform(23-33)
frontend/src/pages/MainPage/components/Banner/Banner.tsx (1)
frontend/src/utils/appStoreLink.ts (1)
detectPlatform(23-33)
frontend/src/pages/MainPage/components/Popup/Popup.styles.ts (2)
frontend/src/styles/zIndex.ts (1)
Z_INDEX(1-6)frontend/src/styles/theme/index.ts (1)
theme(4-7)
🔇 Additional comments (6)
frontend/src/utils/appStoreLink.test.ts (1)
1-153: 훌륭한 테스트 커버리지입니다!appStoreLink 유틸리티에 대한 포괄적인 테스트 커버리지를 제공하고 있습니다:
- 플랫폼 감지 로직(iOS, Android, Other) 전체 커버
- 앱 스토어 링크 반환 로직 검증
- 상수 값 유효성 확인
- Navigator mock 처리가 적절함
테스트 구조가 깔끔하고 모든 엣지 케이스를 다루고 있습니다.
frontend/src/pages/MainPage/components/Banner/Banner.tsx (1)
11-42: 공유 유틸리티로의 리팩터링을 승인합니다!중복된 플랫폼 감지 로직을
appStoreLink유틸리티로 추출한 것은 훌륭한 개선입니다. 이를 통해:
- DRY 원칙 준수
- 코드 중복 제거
- Popup과 Banner 간 플랫폼 감지 로직 일관성 확보
- 향후 유지보수성 향상
frontend/src/constants/eventName.ts (1)
5-9: 새로운 이벤트 상수 추가를 승인합니다!메인 팝업 트래킹을 위한 이벤트 상수들이 잘 정의되어 있습니다:
- 명명 규칙이 기존 패턴과 일관성 있음
- A/B 테스트 및 분석 요구사항 충족
- 팝업의 모든 주요 상호작용(표시, 미표시, 닫기, 다운로드 클릭)을 커버
frontend/src/pages/MainPage/MainPage.tsx (1)
11-55: Popup 컴포넌트의 z-index 레이어링을 확인했습니다.Popup 컴포넌트가 올바르게 추가되었습니다. Z_INDEX 상수가 다음과 같이 정의되어 있어, Popup이 Header보다 위에 표시됩니다:
- Header: 1000
- Overlay: 1100
- Modal: 1200
이 계층 구조로 인해 Popup이 Header 및 다른 컴포넌트 위에 올바르게 렌더링될 것입니다.
frontend/src/pages/MainPage/components/Popup/Popup.test.tsx (1)
1-156: 잘 작성된 테스트입니다!테스트가 포괄적이며 다음을 잘 다루고 있습니다:
- AB 테스트 그룹 할당 및 지속성
- 팝업 숨김 로직 (경계 조건 포함)
- 통합 시나리오
- 엣지 케이스 (잘못된 날짜 형식)
frontend/src/pages/MainPage/components/Popup/Popup.tsx (1)
33-35: 경계 값 처리가 정확한지 확인하세요.
daysSinceHidden < DAYS_TO_HIDE조건은 정확히 7일이 되면 팝업을 다시 표시합니다. 이는 의도된 동작이지만, "7일 동안 숨기기"가 7일 포함을 의미할 수 있으므로 요구사항을 확인하세요.현재 로직:
- 6.99일: 숨김 (true)
- 7.00일: 표시 (false)
만약 "7일 동안"이 7일째까지 포함한다면
<=를 사용해야 합니다.
- handleClose에 action 파라미터 추가 - 닫기 방법(close_button, backdrop_click)을 파라미터로 전달 - handleBackdropClick에서 이벤트 중복 트래킹 제거 - 각 닫기 방법이 정확히 한 번씩만 트래킹되도록 개선
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI Agents
In @frontend/src/pages/MainPage/components/Popup/Popup.tsx:
- Line 14: The type PopupABTestGroup is not exported but is used in tests;
update the declaration of PopupABTestGroup (the union type 'show_popup' |
'no_popup') to be exported (export type PopupABTestGroup = ...) so tests can
import and reference it; ensure any imports in test files are adjusted to import
{ PopupABTestGroup } from the Popup module.
🧹 Nitpick comments (2)
frontend/src/pages/MainPage/components/Popup/Popup.styles.ts (1)
28-35: 테마 색상을 일관되게 사용하세요.Container의
background-color가 하드코딩된#ffffff를 사용하고 있습니다. Line 67의 Button처럼theme.colors.base.white를 사용하여 테마 시스템과 일관성을 유지하는 것이 좋습니다.🔎 제안하는 리팩터링
export const Container = styled.div` display: flex; flex-direction: column; width: 100%; overflow: hidden; - background-color: #ffffff; + background-color: ${theme.colors.base.white}; border-radius: 10px; `;frontend/src/pages/MainPage/components/Popup/Popup.tsx (1)
115-139: 접근성 개선을 고려하세요.렌더링 로직이 올바르게 구현되었습니다.
aria-modal='true'속성이 추가되어 있어 좋습니다.추가로
role="dialog"속성을 Overlay에 추가하면 스크린 리더 사용자에게 더 명확한 정보를 제공할 수 있습니다.🔎 제안하는 개선
<Styled.Overlay isOpen={isOpen} onClick={handleBackdropClick} aria-modal='true' + role='dialog' >
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
frontend/src/pages/MainPage/components/Popup/Popup.styles.tsfrontend/src/pages/MainPage/components/Popup/Popup.tsx
🧰 Additional context used
📓 Path-based instructions (3)
frontend/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.{ts,tsx,js,jsx}: Replace magic numbers with named constants for clarity
Replace complex/nested ternaries withif/elseor IIFEs for readability
Assign complex boolean conditions to named variables for explicit meaning
Avoid hidden side effects; functions should only perform actions implied by their signature (Single Responsibility Principle)
Use unique and descriptive names for custom wrappers/functions to avoid ambiguity
Define constants near related logic or ensure names link them clearly to avoid silent failures
Break down broad state management into smaller, focused hooks/contexts to reduce coupling
Files:
frontend/src/pages/MainPage/components/Popup/Popup.tsxfrontend/src/pages/MainPage/components/Popup/Popup.styles.ts
frontend/**/*.{tsx,jsx}
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.{tsx,jsx}: Abstract complex logic/interactions into dedicated components/HOCs
Separate significantly different conditional UI/logic into distinct components
Colocate simple, localized logic or use inline definitions to reduce context switching
Choose field-level or form-level cohesion based on form requirements when using form libraries like react-hook-form
Use Component Composition instead of Props Drilling to reduce coupling
Files:
frontend/src/pages/MainPage/components/Popup/Popup.tsx
frontend/**/*.{ts,tsx}
📄 CodeRabbit inference engine (frontend/.cursorrules)
Use consistent return types for similar functions/hooks
Files:
frontend/src/pages/MainPage/components/Popup/Popup.tsxfrontend/src/pages/MainPage/components/Popup/Popup.styles.ts
🧠 Learnings (4)
📚 Learning: 2025-11-25T14:08:23.253Z
Learnt from: CR
Repo: Moadong/moadong PR: 0
File: frontend/.cursorrules:0-0
Timestamp: 2025-11-25T14:08:23.253Z
Learning: Applies to frontend/**/*.{ts,tsx,js,jsx} : Define constants near related logic or ensure names link them clearly to avoid silent failures
Applied to files:
frontend/src/pages/MainPage/components/Popup/Popup.tsx
📚 Learning: 2025-11-25T14:08:23.253Z
Learnt from: CR
Repo: Moadong/moadong PR: 0
File: frontend/.cursorrules:0-0
Timestamp: 2025-11-25T14:08:23.253Z
Learning: Applies to frontend/**/*.{ts,tsx} : Use consistent return types for similar functions/hooks
Applied to files:
frontend/src/pages/MainPage/components/Popup/Popup.tsx
📚 Learning: 2025-11-25T14:08:23.253Z
Learnt from: CR
Repo: Moadong/moadong PR: 0
File: frontend/.cursorrules:0-0
Timestamp: 2025-11-25T14:08:23.253Z
Learning: Applies to frontend/**/*.{ts,tsx,js,jsx} : Replace magic numbers with named constants for clarity
Applied to files:
frontend/src/pages/MainPage/components/Popup/Popup.tsx
📚 Learning: 2025-11-25T14:08:23.253Z
Learnt from: CR
Repo: Moadong/moadong PR: 0
File: frontend/.cursorrules:0-0
Timestamp: 2025-11-25T14:08:23.253Z
Learning: Applies to frontend/**/*.{tsx,jsx} : Abstract complex logic/interactions into dedicated components/HOCs
Applied to files:
frontend/src/pages/MainPage/components/Popup/Popup.styles.ts
🧬 Code graph analysis (2)
frontend/src/pages/MainPage/components/Popup/Popup.tsx (2)
frontend/src/constants/eventName.ts (1)
USER_EVENT(1-44)frontend/src/utils/appStoreLink.ts (2)
getAppStoreLink(11-21)detectPlatform(23-33)
frontend/src/pages/MainPage/components/Popup/Popup.styles.ts (2)
frontend/src/styles/zIndex.ts (1)
Z_INDEX(1-6)frontend/src/styles/theme/index.ts (1)
theme(4-7)
🔇 Additional comments (4)
frontend/src/pages/MainPage/components/Popup/Popup.tsx (4)
16-28: LGTM!AB 테스트 그룹 할당 로직이 올바르게 구현되었습니다.
AB_TEST_SPLIT_RATIO상수를 사용하여 매직 넘버 문제가 해결되었습니다.
30-37: LGTM!팝업 숨김 상태 확인 로직이 올바르게 구현되었습니다. localStorage에서 날짜를 읽고
DAYS_TO_HIDE상수와 비교하는 로직이 명확합니다.
73-111: LGTM!이벤트 핸들러들이 올바르게 구현되었습니다. 특히:
handleClose가action파라미터를 받아 닫기 방법을 구분하도록 개선되어 중복 이벤트 트래킹 문제가 해결되었습니다.handleBackdropClick이 올바른 action 값과 함께handleClose를 호출합니다.- 각 핸들러가 적절한 Mixpanel 이벤트를 트래킹합니다.
44-62: Review comment is incorrect - trackEvent는 안정적입니다.
useMixpanelTrack훅에서trackEvent는 빈 의존성 배열([])을 가진useCallback으로 감싸져 있습니다. 따라서trackEvent는 모든 렌더링에서 동일한 함수 참조를 유지하며, useEffect의 의존성 배열에 포함시키는 것이 안전합니다. 현재 구현은 문제가 없습니다.
- 이미지 로딩 상태(imageLoaded) 추가 - Image 객체로 팝업 이미지 사전 로드 - 이미지 로드 완료 후에만 팝업 표시 - 버튼과 이미지가 동시에 렌더링되어 UX 개선
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI Agents
In @frontend/src/pages/MainPage/components/Popup/Popup.tsx:
- Line 14: The type PopupABTestGroup is currently declared but not exported;
export it so tests can import and use it. Update the declaration of
PopupABTestGroup (the union type 'show_popup' | 'no_popup') to be exported
(e.g., export type PopupABTestGroup = ...) and ensure any local references still
compile after adding the export.
- Around line 45-49: The effect that preloads AppDownloadImage never handles
load failures, so imageLoaded can remain false forever; update the useEffect
that creates img (the local img variable in useEffect) to attach an onerror
handler that calls setImageLoaded(true) or sets a separate error state and falls
back to showing the popup, and also clean up handlers in the return function;
locate the useEffect, the img.src = AppDownloadImage line and the setImageLoaded
call to add img.onerror = () => { setImageLoaded(true); /* or
setImageError(true) and handle accordingly */ } plus img.onload and remove
handlers on unmount.
🧹 Nitpick comments (2)
frontend/src/pages/MainPage/components/Popup/Popup.tsx (2)
34-36: 매직 넘버를 상수로 추출하세요.
1000 * 60 * 60 * 24는 하루를 밀리초로 변환하는 값입니다. 코딩 가이드라인에 따라 명명된 상수로 추출하면 가독성이 향상됩니다.🔎 제안된 리팩터링
export const POPUP_STORAGE_KEY = 'mainpage_popup_hidden_date'; export const AB_TEST_KEY = 'mainpage_popup_ab_group'; export const DAYS_TO_HIDE = 7; const AB_TEST_SPLIT_RATIO = 0.5; +const MS_PER_DAY = 1000 * 60 * 60 * 24; // ... export const isPopupHidden = (): boolean => { const hiddenDate = localStorage.getItem(POPUP_STORAGE_KEY); if (!hiddenDate) return false; const daysSinceHidden = - (Date.now() - parseInt(hiddenDate)) / (1000 * 60 * 60 * 24); + (Date.now() - parseInt(hiddenDate)) / MS_PER_DAY; return daysSinceHidden < DAYS_TO_HIDE; };
122-127: 접근성을 위해role="dialog"를 추가하는 것을 고려하세요.
aria-modal='true'가 있지만, 스크린 리더 지원을 위해role="dialog"와aria-labelledby를 함께 사용하면 접근성이 향상됩니다.🔎 제안된 개선
<Styled.Overlay isOpen={isOpen} onClick={handleBackdropClick} + role="dialog" aria-modal='true' + aria-label="앱 다운로드 팝업" >
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
frontend/src/pages/MainPage/components/Popup/Popup.tsx
🧰 Additional context used
📓 Path-based instructions (3)
frontend/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.{ts,tsx,js,jsx}: Replace magic numbers with named constants for clarity
Replace complex/nested ternaries withif/elseor IIFEs for readability
Assign complex boolean conditions to named variables for explicit meaning
Avoid hidden side effects; functions should only perform actions implied by their signature (Single Responsibility Principle)
Use unique and descriptive names for custom wrappers/functions to avoid ambiguity
Define constants near related logic or ensure names link them clearly to avoid silent failures
Break down broad state management into smaller, focused hooks/contexts to reduce coupling
Files:
frontend/src/pages/MainPage/components/Popup/Popup.tsx
frontend/**/*.{tsx,jsx}
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.{tsx,jsx}: Abstract complex logic/interactions into dedicated components/HOCs
Separate significantly different conditional UI/logic into distinct components
Colocate simple, localized logic or use inline definitions to reduce context switching
Choose field-level or form-level cohesion based on form requirements when using form libraries like react-hook-form
Use Component Composition instead of Props Drilling to reduce coupling
Files:
frontend/src/pages/MainPage/components/Popup/Popup.tsx
frontend/**/*.{ts,tsx}
📄 CodeRabbit inference engine (frontend/.cursorrules)
Use consistent return types for similar functions/hooks
Files:
frontend/src/pages/MainPage/components/Popup/Popup.tsx
🧠 Learnings (3)
📚 Learning: 2025-11-25T14:08:23.253Z
Learnt from: CR
Repo: Moadong/moadong PR: 0
File: frontend/.cursorrules:0-0
Timestamp: 2025-11-25T14:08:23.253Z
Learning: Applies to frontend/**/*.{ts,tsx,js,jsx} : Replace magic numbers with named constants for clarity
Applied to files:
frontend/src/pages/MainPage/components/Popup/Popup.tsx
📚 Learning: 2025-11-25T14:08:23.253Z
Learnt from: CR
Repo: Moadong/moadong PR: 0
File: frontend/.cursorrules:0-0
Timestamp: 2025-11-25T14:08:23.253Z
Learning: Applies to frontend/**/*.{ts,tsx} : Use consistent return types for similar functions/hooks
Applied to files:
frontend/src/pages/MainPage/components/Popup/Popup.tsx
📚 Learning: 2025-11-25T14:08:23.253Z
Learnt from: CR
Repo: Moadong/moadong PR: 0
File: frontend/.cursorrules:0-0
Timestamp: 2025-11-25T14:08:23.253Z
Learning: Applies to frontend/**/*.{ts,tsx,js,jsx} : Define constants near related logic or ensure names link them clearly to avoid silent failures
Applied to files:
frontend/src/pages/MainPage/components/Popup/Popup.tsx
🧬 Code graph analysis (1)
frontend/src/pages/MainPage/components/Popup/Popup.tsx (3)
frontend/src/pages/ClubDetailPage/components/PhotoModal/PhotoModal.styles.ts (1)
Image(122-136)frontend/src/constants/eventName.ts (1)
USER_EVENT(1-44)frontend/src/utils/appStoreLink.ts (2)
getAppStoreLink(11-21)detectPlatform(23-33)
🔇 Additional comments (1)
frontend/src/pages/MainPage/components/Popup/Popup.tsx (1)
80-90: LGTM!이벤트 핸들러가
action파라미터를 받아 중복 추적 문제를 해결했습니다. 코드 구조가 깔끔하고 single responsibility를 잘 따르고 있습니다.
#️⃣연관된 이슈
📝작업 내용
메인 페이지 진입 시 모바일 사용자에게 앱 다운로드를 유도하는 팝업을 구현했습니다.
A/B 테스트를 통해 팝업의 효과를 측정할 수 있으며, 사용자 경험을 고려해 "다시보지않기"를 클릭한 후 7일 후 팝업이 뜨도록 구현했습니다.
1. 앱 다운로드 팝업 컴포넌트
useDevice훅을 활용한 반응형 감지2. A/B 테스트 기능
abTestGroup속성을 포함시켜 prop으로 A/B 테스트가 가능하도록 했습니다.3. 다시 보지 않기 기간 설정
localStorage에 날짜를 저장하여 현재 날짜 기준으로 7일이 되었는지, 아니면 지났는지 판단합니다.
4. 플랫폼별 앱 스토어 링크 유틸
중점적으로 리뷰받고 싶은 부분(선택)
논의하고 싶은 부분(선택)
🫡 참고사항
Summary by CodeRabbit
새 기능
스타일
테스트
✏️ Tip: You can customize this high-level summary in your review settings.