Skip to content

[feature] 메인화면에 앱 출시 팝업을 추가한다#1010

Merged
seongwon030 merged 12 commits intodevelop-fefrom
feature/#1009-app-pop-up-MOA-486
Jan 7, 2026
Merged

[feature] 메인화면에 앱 출시 팝업을 추가한다#1010
seongwon030 merged 12 commits intodevelop-fefrom
feature/#1009-app-pop-up-MOA-486

Conversation

@seongwon030
Copy link
Member

@seongwon030 seongwon030 commented Jan 6, 2026

#️⃣연관된 이슈

ex) #1009

📝작업 내용

메인 페이지 진입 시 모바일 사용자에게 앱 다운로드를 유도하는 팝업을 구현했습니다.

A/B 테스트를 통해 팝업의 효과를 측정할 수 있으며, 사용자 경험을 고려해 "다시보지않기"를 클릭한 후 7일 후 팝업이 뜨도록 구현했습니다.

스크린샷 2026-01-06 오후 9 00 14

1. 앱 다운로드 팝업 컴포넌트

  • 모바일 전용: useDevice훅을 활용한 반응형 감지
  • 플랫폼별 자동 연결: iOS/Android 자동 감지 후 해당 앱 스토어로 이동

2. A/B 테스트 기능

  • 50/50 랜덤 분배: 사용자를 show_popup / no_popup 그룹으로 할당했습니다. A/B로 하려다가 확장성을 고려해 기능이 드러나는 이름으로 정했습니다.
  • 모든 이벤트에 abTestGroup 속성을 포함시켜 prop으로 A/B 테스트가 가능하도록 했습니다.

3. 다시 보지 않기 기간 설정

localStorage에 날짜를 저장하여 현재 날짜 기준으로 7일이 되었는지, 아니면 지났는지 판단합니다.

4. 플랫폼별 앱 스토어 링크 유틸

  • 플랫폼 감지: detectPlatform() - iOS/Android/Other
  • 앱 스토어 링크: getAppStoreLink() - 플랫폼별 링크 반환

중점적으로 리뷰받고 싶은 부분(선택)

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?

논의하고 싶은 부분(선택)

논의하고 싶은 부분이 있다면 작성해주세요.

🫡 참고사항

Summary by CodeRabbit

  • 새 기능

    • 모바일 대상 앱 다운로드 팝업 추가(노출·닫기·7일 숨김·AB 테스트 포함)
    • 플랫폼(iOS/Android/기타) 감지 기반 앱스토어 링크 분기 지원
    • 팝업 관련 이벤트 키(열람·비노출·닫기·다운로드 클릭) 추가
  • 스타일

    • 팝업 오버레이 및 모달 스타일 추가·조정
  • 테스트

    • 팝업 동작, 숨김 로직, AB 테스트 및 플랫폼 링크 유틸 단위 테스트 추가

✏️ Tip: You can customize this high-level summary in your review settings.

- 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% 커버리지 달성
@seongwon030 seongwon030 self-assigned this Jan 6, 2026
@seongwon030 seongwon030 added ✨ Feature 기능 개발 💻 FE Frontend labels Jan 6, 2026
@vercel
Copy link

vercel bot commented Jan 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
moadong Ready Ready Preview, Comment Jan 6, 2026 0:34am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 6, 2026

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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.

📥 Commits

Reviewing files that changed from the base of the PR and between c3fe6f1 and e2347df.

📒 Files selected for processing (1)
  • frontend/src/pages/MainPage/components/Popup/Popup.tsx

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Invalid regex pattern for base branch. Received: "**" at "reviews.auto_review.base_branches[0]"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

메인페이지에 모바일 전용 앱 출시 프로모션 팝업을 추가했습니다. AB 테스트 그룹 할당, 7일 숨김 로직(localStorage), 플랫폼 감지 및 앱스토어 링크 유틸리티, Mixpanel 이벤트 추적과 관련 상수·스타일·테스트가 도입되었습니다.

Changes

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/appStoreLinkdetectPlatform/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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@seongwon030 seongwon030 linked an issue Jan 6, 2026 that may be closed by this pull request
2 tasks
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: 코드 중복을 제거하여 유지보수성을 개선하세요.

getAppStoreLinkdetectPlatform 함수가 동일한 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는 다음을 모두 수행합니다:

  1. 팝업이 숨겨져 있는지 확인
  2. AB 테스트 그룹 가져오기
  3. 팝업 열기 결정
  4. 이벤트 추적

코딩 가이드라인에 따르면 복잡한 로직은 더 작고 집중된 훅/컨텍스트로 분리해야 합니다. 이 로직을 커스텀 훅 (예: 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.

📥 Commits

Reviewing files that changed from the base of the PR and between b11d447 and 0662b7b.

⛔ Files ignored due to path filters (1)
  • frontend/src/assets/images/popup/app-download.svg is excluded by !**/*.svg
📒 Files selected for processing (8)
  • frontend/src/constants/eventName.ts
  • frontend/src/pages/MainPage/MainPage.tsx
  • frontend/src/pages/MainPage/components/Banner/Banner.tsx
  • frontend/src/pages/MainPage/components/Popup/Popup.styles.ts
  • frontend/src/pages/MainPage/components/Popup/Popup.test.tsx
  • frontend/src/pages/MainPage/components/Popup/Popup.tsx
  • frontend/src/utils/appStoreLink.test.ts
  • frontend/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 with if/else or 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.ts
  • frontend/src/utils/appStoreLink.test.ts
  • frontend/src/utils/appStoreLink.ts
  • frontend/src/pages/MainPage/components/Popup/Popup.test.tsx
  • frontend/src/pages/MainPage/MainPage.tsx
  • frontend/src/pages/MainPage/components/Popup/Popup.tsx
  • frontend/src/pages/MainPage/components/Banner/Banner.tsx
  • frontend/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.ts
  • frontend/src/utils/appStoreLink.test.ts
  • frontend/src/utils/appStoreLink.ts
  • frontend/src/pages/MainPage/components/Popup/Popup.test.tsx
  • frontend/src/pages/MainPage/MainPage.tsx
  • frontend/src/pages/MainPage/components/Popup/Popup.tsx
  • frontend/src/pages/MainPage/components/Banner/Banner.tsx
  • frontend/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.tsx
  • frontend/src/pages/MainPage/MainPage.tsx
  • frontend/src/pages/MainPage/components/Popup/Popup.tsx
  • frontend/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에서 이벤트 중복 트래킹 제거
- 각 닫기 방법이 정확히 한 번씩만 트래킹되도록 개선
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

📥 Commits

Reviewing files that changed from the base of the PR and between 0662b7b and a3ac814.

📒 Files selected for processing (2)
  • frontend/src/pages/MainPage/components/Popup/Popup.styles.ts
  • 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 with if/else or 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/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.tsx
  • frontend/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!

이벤트 핸들러들이 올바르게 구현되었습니다. 특히:

  • handleCloseaction 파라미터를 받아 닫기 방법을 구분하도록 개선되어 중복 이벤트 트래킹 문제가 해결되었습니다.
  • handleBackdropClick이 올바른 action 값과 함께 handleClose를 호출합니다.
  • 각 핸들러가 적절한 Mixpanel 이벤트를 트래킹합니다.

44-62: Review comment is incorrect - trackEvent는 안정적입니다.

useMixpanelTrack 훅에서 trackEvent는 빈 의존성 배열([])을 가진 useCallback으로 감싸져 있습니다. 따라서 trackEvent는 모든 렌더링에서 동일한 함수 참조를 유지하며, useEffect의 의존성 배열에 포함시키는 것이 안전합니다. 현재 구현은 문제가 없습니다.

- 이미지 로딩 상태(imageLoaded) 추가
- Image 객체로 팝업 이미지 사전 로드
- 이미지 로드 완료 후에만 팝업 표시
- 버튼과 이미지가 동시에 렌더링되어 UX 개선
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

📥 Commits

Reviewing files that changed from the base of the PR and between a3ac814 and c3fe6f1.

📒 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 with if/else or 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를 잘 따르고 있습니다.

Copy link
Member

@oesnuj oesnuj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나는 당신을 믿어요

@seongwon030 seongwon030 merged commit d4f5520 into develop-fe Jan 7, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AB TEST AB 테스트 관련 💻 FE Frontend ✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feature] MOA-486 메인화면에 앱 출시 팝업을 추가한다

2 participants