Skip to content

[release] FE v1.7.4#1560

Merged
suhyun113 merged 92 commits into
mainfrom
develop-fe
May 22, 2026
Merged

[release] FE v1.7.4#1560
suhyun113 merged 92 commits into
mainfrom
develop-fe

Conversation

@seongwon030
Copy link
Copy Markdown
Member

@seongwon030 seongwon030 commented May 17, 2026

🚀 릴리즈 PR

📦 버전 정보

항목 내용
서비스 💻 FE
Bump 타입 🔧 PATCH

⚠️ 반드시 라벨을 지정해주세요: 서비스 라벨(💾 BE, 💻 FE)과 버전 라벨(🚨 MAJOR, ➕ MINOR, 🔧 PATCH)이 없으면 태그가 생성되지 않습니다.

📖 버전 라벨 선택 가이드 (Semantic Versioning)
라벨 버전 변화 선택 기준 예시
🚨 MAJOR v1.0.0v2.0.0 기존 API/기능이 호환되지 않는 변경 API 엔드포인트 삭제/변경, 요청/응답 스펙 변경, DB 스키마 대규모 변경
➕ MINOR v1.0.0v1.1.0 기존 기능은 유지하면서 새 기능 추가 새 API 엔드포인트 추가, 새 기능 도입, 기존 API에 선택적 필드 추가
🔧 PATCH v1.0.0v1.0.1 기능 변경 없이 버그 수정/내부 개선 버그 수정, 성능 개선, 리팩토링, 문서 수정

📋 포함된 변경사항

✨ 신규 기능

  • 클럽 상세 OG 태그 — Vercel Edge Middleware로 카카오톡·페이스북 등 SNS 공유 시 클럽 정보 미리보기 지원. @clubName 형식·한글 URL 디코딩·3초 타임아웃 포함
  • 배너 shimmer 스켈레톤 UI — API 응답 대기 중뿐 아니라 이미지 로드 완료 시점까지 스켈레톤 유지
  • 동아리 커버 이미지 분과별 색상 폴백 — 커버 이미지가 없을 때 분과 태그 색상으로 대체

📊 트래킹 강화

  • 클럽카드 노출 히트맵 — IntersectionObserver + 실제 체류시간(dwell_ms) 측정, 300ms 미만 fly-by 필터링, view_count·reentry_count 세션 집계
  • 스크롤 깊이 트래킹 — 메인페이지 25/50/75/100% 마일스톤 이벤트 (useScrollTracking 훅)
  • 배너 네비게이션 트래킹 — 이전/다음 버튼 클릭 시 from_index 기록
  • 네이버 맵 클릭 트래킹 — 지도 카드·프로필 카드 두 진입점 모두 수집
  • 누락 이벤트 보완 — 뒤로가기 버튼, 모바일 메뉴 열기, 검색 실행 이벤트 추가

🔧 개선

  • React Query 캐싱 재조정useBanner 24h→1h (긴급 배너 교체 대응), usePromotion 5min→1min (refetchInterval 기준 정렬)
  • 동아리 프로필 로고 테두리 — 데스크탑/모바일 반응형 분리 적용

🐛 버그 수정

  • 동아리방 위치 텍스트 오버플로우로 지도 버튼이 가려지는 문제 수정
  • PromotionClubCTA 웹뷰 네비게이션 @clubName 기반으로 통일, bridge 실패 시 fallback 추가
  • 웹뷰 메인페이지 가로 스크롤 방지
  • 홍보게시판 행사 카드 위치/시간 아이콘 크기·반응형 수정
  • 배너 첫 이미지 로드 실패 시 깨진 이미지 숨김 처리

🛠 개발 도구

  • Claude 커맨드 추가/cross-review (크로스 에이전트 코드 리뷰), /jira-story, /jira-task (Jira 스토리·하위작업 자동 생성), /prd (PRD 기반 하위작업 일괄 생성), /check-tracking (트래킹 감사), /RN (RN 에이전트 위임)
  • RN 서브에이전트 — React Native 전담 에이전트 파일 팀 공유 추가
  • PR 자동 라벨 — GitHub Actions로 PR 제목 기반 라벨 자동 지정

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 배너 로딩 중 스켈레톤 UI 표시로 개선된 사용자 경험 제공
    • 소셜 미디어 공유 시 동아리 정보 미리보기 표시 지원
    • 스크롤 깊이 추적으로 더 정확한 사용자 행동 분석 가능
  • Improvements

    • 배너 및 프로모션 데이터 캐시 효율성 최적화
    • 동아리 상세 페이지 커버 이미지 표시 방식 개선
    • 모바일 메뉴 및 뒤로가기 버튼 추적 강화
    • 카드 클릭 이벤트 추적 데이터 상세화
  • Bug Fixes

    • WebView 브릿지 누락 시 명확한 에러 처리 추가

suhyun113 and others added 30 commits May 13, 2026 17:19
LocationInfo에 min-width: 0과 overflow: hidden을 추가하여
모바일에서 긴 위치 텍스트가 지도 링크를 밀어내는 현상 해결
- CLUB_CARD_VIEWED, SCROLL_DEPTH_REACHED 이벤트 상수 추가
- PAGE_NAME 상수 추가 (main, webview-main, introduce)
- BREAKPOINT 기준 device_type 판별 유틸 추가 (mini_mobile/mobile/tablet/laptop/desktop)
- useScrollTracking 훅 구현 (25/50/75/100% 마일스톤)
- passive scroll 이벤트 리스너로 성능 최적화
- page 의존성 배열 추가 및 reached ref 리셋으로 stale closure 방지
- MainPage에 useScrollTracking 적용
- IntersectionObserver + 3초 dwell timer로 CLUB_CARD_VIEWED 이벤트 수집
- ([entry]) 구조 분해로 단일 엔트리 처리 (forEach 배치 버그 수정)
- 클릭 시 scroll_y, card_top_in_viewport, device_type 수집
- page prop으로 main/webview-main/introduce 페이지 구분
- MainPage, WebviewMainPage, IntroSection에 index·page prop 적용
- ClubCard Viewed/Clicked, Scroll Depth Reached 이벤트 설명
- 뷰포트 히트맵, CTR, 스크롤 퍼널 등 5가지 분석 시나리오 정리
- docs/features/main/ → docs/features/event/ 로 이동
- 세 이벤트 테이블에 device_type, page 속성 추가
- getDeviceType.ts 관련 코드 섹션에 추가
- mixpanel.md 업데이트 (카드 히트맵 트래킹 시나리오 반영)
- mixpanel-ab.md 추가 (A/B 테스트 유의성 검정 가이드)
- 모아동 자체 실험 프레임워크(Super Property 방식) 설명 추가
- $experiment_started 기반 쿼리 → experiment.key 프로퍼티 breakdown으로 수정
- DAU 500명 기준 현실적 샘플 크기 계산 추가
- Bonferroni 보정 외부 계산기 활용 방법 추가
- 카드 트래킹 데이터 기반 Primary Metric 예시 및 쿼리 추가
- Sequential Testing 문구 제거 (자체 프레임워크에서 미지원)
- .gitignore: .claude/commands/ 팀 공유 예외 처리
- CLAUDE.md: skill routing 규칙 추가
- .claude/commands/cross-review.md: /cross-review 슬래시 커맨드
- scripts/cross-review.sh: claude/codex 자동 감지, --writer 플래그로 반대 에이전트가 리뷰
- .claude/commands/jira-story.md: /jira-story 슬래시 커맨드 (대화형)
- scripts/jira-story.sh: Jira REST API 호출, 스프린트 지정 지원, .env 자동 로드
카카오톡/페이스북 등 크롤러 요청 시 클럽 정보를 API에서 fetch하여
OG 태그가 포함된 HTML을 반환하는 Vercel Edge Middleware 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ObjectId만 매칭하던 regex를 @clubname 형식도 포함하도록 수정

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Vercel Edge 5초 실행 제한 대응, API 지연 시 SPA로 fallback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-856

[feature] 클럽 상세 페이지 OG 태그 Edge Middleware 적용
JIRA_HOST, PROJECT_KEY 하드코딩 제거 및 .env / shell 환경 변수에서 로드하도록 변경.
누락 시 명확한 오류 메시지와 함께 종료하는 유효성 검사 추가.
- 네트워크 행 방지를 위해 --connect-timeout 5, --max-time 30 추가
- base64 수동 인코딩 제거, curl -u 옵션으로 이식성 보장
…jira-story-MOA-854

[feature] 클로드 커맨드 추가 (cross-review, jira)
정규식 끝에 $ 앵커가 없어 /club/:clubId 하위 경로에도
OG HTML이 반환되던 버그 수정 (MOA-859)
percent-encoded 경로(@%EB...)를 디코딩하지 않아 API 호출 실패 및
og:url에 인코딩된 URL이 노출되던 문제 수정 (MOA-859)
잘못된 percent-encoding 입력 시 URIError가 try 블록 밖에서
발생하여 Edge Function이 500으로 터지는 문제 방지
잘못된 인코딩이어도 원본 문자열로 API 시도 후 fallback
…bug-MOA-860

[fix] OG 미들웨어 정규식 끝 앵커 누락 수정
…l-scroll-webview-MOA-830

fix(webview): 웹뷰 메인페이지 좌우 스크롤 방지 및 렌더링 개선
Copy link
Copy Markdown
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: 7

🧹 Nitpick comments (6)
frontend/src/utils/getDeviceType.ts (1)

3-5: ⚡ Quick win

DeviceType 타입을 공용 타입으로 통일하세요.

현재 유틸 내부에서 DeviceType을 재선언하고 있어 타입 소스가 분산됩니다. 기존 공용 타입(@/types/device)을 import해서 일관성 유지가 좋습니다.

제안 diff
 import { BREAKPOINT } from '`@/styles/mediaQuery`';
+import { DeviceType } from '`@/types/device`';

-type DeviceType = 'mini_mobile' | 'mobile' | 'tablet' | 'laptop' | 'desktop';
-
 const getDeviceType = (): DeviceType => {

As per coding guidelines, "React + TypeScript + Vite 기반 프론트엔드에서 타입이 필요하면 기존 타입 선언 위치와 네이밍 규칙을 따른다".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/utils/getDeviceType.ts` around lines 3 - 5, The file redeclares
the DeviceType type locally; remove the local declaration and import the shared
DeviceType from the existing module (e.g., import { DeviceType } from
'`@/types/device`'), then update the getDeviceType signature and any usages to use
that imported DeviceType; ensure there are no remaining local type declarations
and run/type-check to confirm compatibility with the shared type.
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (1)

158-161: ⚡ Quick win

지도 클릭 액션 핸들러를 하나로 합치세요.

동일한 로직이 두 군데 반복되어 이후 이벤트 속성 변경 시 누락 가능성이 있습니다. 공통 핸들러로 추출하는 편이 안전합니다.

제안 diff
+  const handleMapClick = useCallback(() => {
+    setIsMapModalOpen(true);
+    trackEvent(USER_EVENT.CLUB_MAP_CLICKED);
+  }, [trackEvent]);

             <ClubProfileCard
@@
-              onMapClick={() => {
-                setIsMapModalOpen(true);
-                trackEvent(USER_EVENT.CLUB_MAP_CLICKED);
-              }}
+              onMapClick={handleMapClick}
             />
@@
-                <Styled.MapCard
-                  onClick={() => {
-                    setIsMapModalOpen(true);
-                    trackEvent(USER_EVENT.CLUB_MAP_CLICKED);
-                  }}
-                >
+                <Styled.MapCard onClick={handleMapClick}>

Also applies to: 165-170

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx` around lines 158 - 161,
Extract the inline onMapClick logic into a single handler (e.g., handleMapClick)
inside the ClubDetailPage component and replace both inline handlers with that
function; the handler should call setIsMapModalOpen(true) and
trackEvent(USER_EVENT.CLUB_MAP_CLICKED) so both usages share identical behavior
and future changes require only one edit (update the onMapClick props that
currently call setIsMapModalOpen and trackEvent to reference handleMapClick).
frontend/src/constants/eventName.ts (1)

3-3: ⚡ Quick win

이벤트 키 오탈자를 지금 정리해주세요 (SEARCH_EXECUTED)

Line 3의 SEARCH_EXCUTED는 오탈자라, 참조가 더 늘어나기 전에 정리하는 편이 안전합니다.

✏️ 제안 수정
-  SEARCH_EXCUTED: 'Search Executed',
+  SEARCH_EXECUTED: 'Search Executed',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/constants/eventName.ts` at line 3, The constant key in
frontend/src/constants/eventName.ts is misspelled as SEARCH_EXCUTED; rename it
to SEARCH_EXECUTED in the eventNames object (and preserve the value string if
desired), then update all usages/imports throughout the codebase to reference
SEARCH_EXECUTED (search for SEARCH_EXCUTED to find callers), run
TypeScript/compile checks and any tests to ensure no remaining references break,
and adjust any dependent event-string comparisons or analytics usages to the
corrected symbol.
frontend/.claude/commands/prd.md (3)

70-70: ⚡ Quick win

순차 실행이 필요한 이유를 문서화하세요.

"병렬 실행 금지"라고 명시되어 있지만, 그 이유가 설명되지 않았습니다. 이는 Jira API의 rate limiting, GitHub Actions의 동시성 제약, 또는 브랜치 생성 순서 의존성 등 중요한 아키텍처 제약을 나타낼 수 있습니다. 사용자가 왜 순차적으로 실행해야 하는지 이해할 수 있도록 간단한 설명을 추가하는 것이 좋습니다.

💡 제안하는 추가 설명 예시
-하위 작업 목록을 **순서대로 하나씩** 실행합니다 (병렬 실행 금지):
+하위 작업 목록을 **순서대로 하나씩** 실행합니다 (GitHub Actions webhook 처리 순서 보장을 위해 병렬 실행 금지):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/.claude/commands/prd.md` at line 70, 문서의 "하위 작업 목록을 **순서대로 하나씩**
실행합니다 (병렬 실행 금지):" 문구에 순차 실행이 반드시 필요한 구체적 이유를 한두 문장으로 추가하세요 — 예: Jira API rate
limiting, GitHub Actions 동시성/자원 충돌, 브랜치 생성/머지 순서 의존성, 또는 데이터 레이스 방지 등 해당 워크플로우에
적용되는 하나 또는 두 가지 핵심 원인을 명시하고, 필요한 경우 권장 대체(예: 백오프/재시도 전략)를 짧게 덧붙이세요.

73-73: ⚡ Quick win

6번째 매개변수(빈 문자열)의 용도를 문서화하세요.

jira-task.sh 호출 시 6번째 위치에 빈 문자열("")을 전달하고 있지만, 이 매개변수가 무엇을 의미하는지 설명이 없습니다. 선택적 설명 필드인지, 레이블인지, 아니면 향후 사용을 위한 예약 필드인지 명확히 문서화하면 사용자가 필요에 따라 이 필드를 활용할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/.claude/commands/prd.md` at line 73, Document the purpose of the 6th
positional parameter (currently passed as an empty string) for the jira-task.sh
invocation: update the example line using ./scripts/jira-task.sh and add a short
description in the parameter list that clearly states whether it is an optional
description field, a label, a reserved/unused slot, or how it will be
interpreted by jira-task.sh; mention its default behavior when empty and an
example usage if non-empty so users know when to provide a value.

102-102: ⚡ Quick win

쌍따옴표 관련 경고를 더 명확히 하세요.

현재 경고는 "태스크 문자열에 쌍따옴표가 포함되면 bash 오류 발생"이라고 되어 있지만, 위의 명령 예시들(62행, 73행)에서는 모든 매개변수를 쌍따옴표로 감싸고 있어 혼란을 줄 수 있습니다. 경고의 의도는 태스크 내용 안에 쌍따옴표가 있을 때 문제가 발생한다는 것이므로, 이를 더 명확히 표현하는 것이 좋습니다.

💡 제안하는 명확화
-- 태스크 문자열에 쌍따옴표(`"`)가 포함되면 bash 오류 발생 → 홑따옴표 또는 제거
+- 태스크 **내용 안에** 쌍따옴표(`"`)가 포함되면 bash 오류 발생 → 홑따옴표 또는 제거
+  - 예: `"- [ ] "동아리" 페이지 생성"` (❌) → `"- [ ] '동아리' 페이지 생성"` (✅)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/.claude/commands/prd.md` at line 102, 현재 경고 문자열 '태스크 문자열에 쌍따옴표(`"`)가
포함되면 bash 오류 발생 → 홑따옴표 또는 제거'가 모호하니 이 문구를 명확하게 바꿔 태스크의 "내용 안에" 쌍따옴표가 있을 때 문제가
발생한다고 설명하고, 해결책으로 태스크 전체를 홑따옴표로 감싸거나 내부 쌍따옴표를 이스케이프(혹은 제거)하라고 명시하세요; 또한 위의
예시들(현재 문서의 62행/73행에 해당하는 명령 예시)이 모든 매개변수를 쌍따옴표로 감싸 혼동을 주므로 해당 예시는 홑따옴표 사용 또는
이스케이프된 쌍따옴표 버전으로 고쳐 문구와 일치시키세요.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/.claude/agents/RN에이전트.md`:
- Around line 24-35: The fenced code block in RN에이전트.md (the directory tree
between app/ and modal.tsx) lacks a language tag causing MD040; add a language
identifier (for example "text") to the opening triple-backticks for that block
so it becomes ```text, which will satisfy the linter and remove the warning.

In `@frontend/.claude/commands/check-tracking.md`:
- Around line 12-13: 현재 문서의 grep 명령들이 상대 경로 `src/...`를 사용해 실행 위치에 따라 결과가 달라질 수
있으니 모든 해당 명령(grep -E "^\s+[A-Z_]+:" src/constants/eventName.ts 등)을 프로젝트 루트가 아닌
frontend 디렉터리를 기준으로 고정하도록 변경하세요; 즉 문서의 명령을 `frontend/src/constants/eventName.ts`
형태로 수정하거나 문서 시작에 “frontend 디렉터리에서 실행”을 명시해서 12–13, 20–21, 28–29, 34–35 구간의 모든
grep 예제가 일관된 경로를 가리키게 해 주세요.

In `@frontend/.claude/commands/jira-task.md`:
- Around line 31-46: The fenced code block opening with ``` in the Jira task
example is missing a language tag, triggering markdownlint MD040; fix by adding
a language identifier (e.g., change ``` to ```text) on that fenced code block so
the example output is marked as plain text and the MD040 warning is resolved.

In `@frontend/.claude/commands/mixpanel-ab.md`:
- Around line 43-190: Add explicit fenced-code language identifiers to each code
block in the markdown to fix MD040 lint warnings: update the block that begins
with "⚠️ Mixpanel은..." to use ```text, the bash command block starting with
"mcp-cli info claude_ai_mixpanel/Run-Query" to use ```bash, and similarly tag
the other plain-text/example blocks (sample-size calculations, group math,
checklist snippets like "❌ 잘못된 방법" sections, and the numbered step list) with
either ```text or ```bash as appropriate so every fenced block has a language
specifier.

In `@frontend/.claude/commands/prd.md`:
- Around line 43-48: Add a language identifier to the fenced code block that
contains the markdown table (the triple-backtick block with the table starting
"| # | 제목 | 브랜치명 | 태스크 |") by changing the opening fence from ``` to ```markdown
so the block is recognized as Markdown; ensure only the opening fence is
modified and the table content remains unchanged.
- Around line 86-95: The fenced code block in frontend/.claude/commands/prd.md
that shows the completion example (the block starting with "✅ 스토리: MOA-xxx —
https://...atlassian.net/browse/MOA-xxx") is missing a language identifier;
update the opening fence to include an explicit language token such as ```text
(or ```md) so the block reads ```text and the closing fence remains ```,
ensuring syntax highlighting and linting tools recognize it.

In `@frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx`:
- Around line 42-61: The impression timer is being recreated on every
entry.isIntersecting callback and old handles are lost; modify the ClubCard
intersection handler so you only call setTimeout to create dwellTimer when there
is not already an active timer (check dwellTimer is null/undefined) before
scheduling the 3000ms callback that sets hasTrackedImpression.current and calls
trackEvent(USER_EVENT.CLUB_CARD_VIEWED, ...). Keep the existing
clearTimeout(dwellTimer); dwellTimer = null; branch to cancel timers when not
intersecting, and make sure the timer callback also guards with
hasTrackedImpression.current to avoid duplicate tracking.

---

Nitpick comments:
In `@frontend/.claude/commands/prd.md`:
- Line 70: 문서의 "하위 작업 목록을 **순서대로 하나씩** 실행합니다 (병렬 실행 금지):" 문구에 순차 실행이 반드시 필요한 구체적
이유를 한두 문장으로 추가하세요 — 예: Jira API rate limiting, GitHub Actions 동시성/자원 충돌, 브랜치
생성/머지 순서 의존성, 또는 데이터 레이스 방지 등 해당 워크플로우에 적용되는 하나 또는 두 가지 핵심 원인을 명시하고, 필요한 경우 권장
대체(예: 백오프/재시도 전략)를 짧게 덧붙이세요.
- Line 73: Document the purpose of the 6th positional parameter (currently
passed as an empty string) for the jira-task.sh invocation: update the example
line using ./scripts/jira-task.sh and add a short description in the parameter
list that clearly states whether it is an optional description field, a label, a
reserved/unused slot, or how it will be interpreted by jira-task.sh; mention its
default behavior when empty and an example usage if non-empty so users know when
to provide a value.
- Line 102: 현재 경고 문자열 '태스크 문자열에 쌍따옴표(`"`)가 포함되면 bash 오류 발생 → 홑따옴표 또는 제거'가 모호하니 이
문구를 명확하게 바꿔 태스크의 "내용 안에" 쌍따옴표가 있을 때 문제가 발생한다고 설명하고, 해결책으로 태스크 전체를 홑따옴표로 감싸거나 내부
쌍따옴표를 이스케이프(혹은 제거)하라고 명시하세요; 또한 위의 예시들(현재 문서의 62행/73행에 해당하는 명령 예시)이 모든 매개변수를
쌍따옴표로 감싸 혼동을 주므로 해당 예시는 홑따옴표 사용 또는 이스케이프된 쌍따옴표 버전으로 고쳐 문구와 일치시키세요.

In `@frontend/src/constants/eventName.ts`:
- Line 3: The constant key in frontend/src/constants/eventName.ts is misspelled
as SEARCH_EXCUTED; rename it to SEARCH_EXECUTED in the eventNames object (and
preserve the value string if desired), then update all usages/imports throughout
the codebase to reference SEARCH_EXECUTED (search for SEARCH_EXCUTED to find
callers), run TypeScript/compile checks and any tests to ensure no remaining
references break, and adjust any dependent event-string comparisons or analytics
usages to the corrected symbol.

In `@frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx`:
- Around line 158-161: Extract the inline onMapClick logic into a single handler
(e.g., handleMapClick) inside the ClubDetailPage component and replace both
inline handlers with that function; the handler should call
setIsMapModalOpen(true) and trackEvent(USER_EVENT.CLUB_MAP_CLICKED) so both
usages share identical behavior and future changes require only one edit (update
the onMapClick props that currently call setIsMapModalOpen and trackEvent to
reference handleMapClick).

In `@frontend/src/utils/getDeviceType.ts`:
- Around line 3-5: The file redeclares the DeviceType type locally; remove the
local declaration and import the shared DeviceType from the existing module
(e.g., import { DeviceType } from '`@/types/device`'), then update the
getDeviceType signature and any usages to use that imported DeviceType; ensure
there are no remaining local type declarations and run/type-check to confirm
compatibility with the shared type.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b1ca2c23-aa3c-4e77-b780-df319c738807

📥 Commits

Reviewing files that changed from the base of the PR and between 904297f and 1cedf9b.

⛔ Files ignored due to path filters (1)
  • frontend/src/assets/images/icons/location_icon.svg is excluded by !**/*.svg
📒 Files selected for processing (36)
  • frontend/.claude/agents/RN에이전트.md
  • frontend/.claude/commands/RN.md
  • frontend/.claude/commands/check-tracking.md
  • frontend/.claude/commands/jira-story.md
  • frontend/.claude/commands/jira-task.md
  • frontend/.claude/commands/mixpanel-ab.md
  • frontend/.claude/commands/mixpanel.md
  • frontend/.claude/commands/prd.md
  • frontend/.gitignore
  • frontend/docs/claude/api.md
  • frontend/docs/claude/features.md
  • frontend/docs/features/club-detail/map-event-tracking.md
  • frontend/docs/features/event/card-heatmap-tracking.md
  • frontend/docs/superpowers/specs/2026-04-14-design-system-toolkit-design.md
  • frontend/scripts/cross-review.sh
  • frontend/scripts/jira-story.sh
  • frontend/scripts/jira-task.sh
  • frontend/src/components/common/Header/Header.tsx
  • frontend/src/constants/eventName.ts
  • frontend/src/hooks/Header/useHeaderNavigation.ts
  • frontend/src/hooks/Mixpanel/useMixpanelTrack.ts
  • frontend/src/hooks/Mixpanel/useScrollTracking.ts
  • frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubProfileCard/ClubProfileCard.styles.ts
  • frontend/src/pages/ClubDetailPage/components/ClubProfileCard/ClubProfileCard.tsx
  • frontend/src/pages/IntroducePage/components/sections/1.IntroSection/IntroSection.tsx
  • frontend/src/pages/MainPage/MainPage.tsx
  • frontend/src/pages/MainPage/components/Banner/Banner.tsx
  • frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx
  • frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx
  • frontend/src/pages/PromotionPage/components/detail/PromotionDetailTopBar/PromotionDetailTopBar.tsx
  • frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.styles.ts
  • frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.tsx
  • frontend/src/pages/WebviewMainPage/WebviewMainPage.tsx
  • frontend/src/utils/getDeviceType.ts
💤 Files with no reviewable changes (3)
  • frontend/docs/superpowers/specs/2026-04-14-design-system-toolkit-design.md
  • frontend/docs/claude/api.md
  • frontend/src/hooks/Mixpanel/useMixpanelTrack.ts
✅ Files skipped from review due to trivial changes (8)
  • frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx
  • frontend/.claude/commands/RN.md
  • frontend/.claude/commands/mixpanel.md
  • frontend/docs/features/club-detail/map-event-tracking.md
  • frontend/.gitignore
  • frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.styles.ts
  • frontend/docs/claude/features.md
  • frontend/docs/features/event/card-heatmap-tracking.md

Comment thread frontend/.claude/agents/RN에이전트.md
Comment thread frontend/.claude/commands/check-tracking.md
Comment thread frontend/.claude/commands/jira-task.md
Comment thread frontend/.claude/commands/mixpanel-ab.md
Comment thread frontend/.claude/commands/prd.md
Comment thread frontend/.claude/commands/prd.md
Comment thread frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx Outdated
- 3초 dwell 임계값 제거 → 진입/이탈 기반 실제 체류시간(dwell_ms) 측정
- sessionStorage로 view_count·reentry_count 탭 세션 집계 (page 스코프)
- visibilitychange로 탭 종료 시 이벤트 유실 방지
- cooldown 15s → 2s (jitter 방지 목적으로 축소)
- BANNER_NAVIGATION_CLICKED 이벤트 추가 (direction: prev/next, current_index)
read-only 리뷰 전용 서브커맨드 사용으로 저장소 수정 방지
seongwon030 and others added 5 commits May 21, 2026 13:50
ClubCard 등 다른 컴포넌트와 일관성을 맞추기 위해 동아리 상세 이동 시
clubId 대신 @clubname 패턴 사용으로 변경

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ClubCard: isIntersecting 재호출 시 intersectStart null 가드 추가 (dwell_ms 과소 측정 버그 수정)
- club-card-tracking.md: 코드 펜스에 text 언어 지정 (MD040)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…bview-nav-MOA-886

[fix] PromotionClubCTA 웹뷰 네비게이션 동작 수정
seongwon030 and others added 3 commits May 21, 2026 21:39
slidePrev/slideNext 호출 직후 currentIndex는 아직 이전 슬라이드를 가리키므로
출발 슬라이드 인덱스임을 명확히 하기 위해 프로퍼티명 변경

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
안구 고정 최소 지속 시간(~200ms, Rayner 1998) 기반으로 300ms 미만 체류는
이벤트 미발생 처리. intersectStart 초기화를 dwell 체크 전으로 이동해
fly-by 시에도 상태 항상 초기화 및 쿨다운 미발동 보장.
…-tracking-MOA-899

[feature] 클럽카드 impression 트래킹 전면 개편 및 배너 네비게이션 트래킹 추가
@github-actions github-actions Bot added the 🚁AI claude, codex 활용 시 label May 22, 2026
Comment thread frontend/src/hooks/Mixpanel/useScrollTracking.ts Outdated

<Styled.SocialLinksWrapper>
{getActiveSocials().map(({ platform, url }) => {
const icon = getSocialIcon(platform);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

현재 인스타그램과 유튜브 링크만 보이도록 되어있는데, 관리자 페이지에는 X 링크도 있어서 개선해야겠네요.

Comment thread frontend/middleware.ts Outdated
image: club.cover || club.logo || DEFAULT_OG_IMAGE,
url: `${SITE_URL}${safeDecode(pathname)}`,
}),
{ headers: { 'content-type': 'text/html; charset=utf-8' } },
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

OG 미들웨어가 크롤러 요청마다 매번 백엔드 API를 호출하고 있습니다. 같은 동아리 링크가 카카오톡 채팅방에서 여러 번 공유될 경우 동일한 데이터를 중복을 fetch하는 문제가 있을 것 같습니다

응답에 cache-control: public, s-maxage=300, stale-while-revalidate=60 헤더를 추가했습니다 9eb0385

Vercel Edge CDN이 OG 응답을 5분간 캐싱하도록 수정하여 같은 URL에 대한 백엔드 API 호출을 1회로 줄이고자 했습니다! 5분 만료 이후에도 60초간은 기존 응답을 내려주면서 백그라운드에서 갱신하도록 하여 크롤러가 빈 응답을 받을 확률은 감소시켰습니다!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

좋은 방법이네요 ! 먼가 활발하게 수정되는 시기와 아닌 시기에 따라 캐싱 시간을 다르게 두는 방법도 있을 것 같은데
나중에 한 번 얘기해보시죠

@suhyun113
Copy link
Copy Markdown
Collaborator

릴리즈 하겠습니당

Copy link
Copy Markdown
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: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/pr-auto-label.yml:
- Line 17: 현재 워크플로우에서 사용 중인 actions/github-script@v7 태그는 업스트림 변경에 따라 동작이 바뀔 수
있으니, 'uses: actions/github-script@v7'를 해당 액션 리포지토리의 특정 커밋 SHA로 교체하여 핀닝하세요;
.github/workflows/pr-auto-label.yml에서 'uses' 항목을 찾아 actions/github-script 레포에서
v7 태그와 대응하는 최신 커밋 SHA로 바꾸고 커밋 SHA 문자열을 사용해 고정하면 됩니다.

In `@frontend/middleware.ts`:
- Around line 86-91: 응답 헤더 블록(현재 headers 객체)에 Vary: user-agent를 추가하여
CRAWLER_PATTERN에 매칭되는 봇용 OG HTML을 반환할 때 공유 캐시가 다른 User-Agent에 대해 봇 HTML을 재사용하지
않도록 하세요; middleware.ts의 CRAWLER_PATTERN 판정 로직이 OG HTML을 선택하는 분기(및 해당 Response 생성
시 사용하는 headers 객체)를 찾아 'Vary': 'user-agent' 헤더를 병합해 포함하도록 수정하면 됩니다.

In `@frontend/src/pages/MainPage/components/Banner/Banner.tsx`:
- Around line 45-59: The tracking fires even when swiperInstance is null; update
handlePrev and handleNext to only call trackEvent after confirming the swiper
instance exists and the navigation method was invoked: check swiperInstance (and
optional method existence) before calling swipe actions (e.g., in handlePrev
verify swiperInstance && swiperInstance.slidePrev()) and then call
trackEvent(USER_EVENT.BANNER_NAVIGATION_CLICKED, { direction: 'prev'|'next',
from_index: currentIndex }) inside that guarded branch so events are only sent
when the slide navigation actually occurred.

In `@frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx`:
- Around line 77-91: The intersection callback currently gates impressions using
entry.isIntersecting, which ignores the intended IMPRESSION_THRESHOLD; change
the condition to check entry.intersectionRatio >= IMPRESSION_THRESHOLD (in
addition to or instead of isIntersecting) before starting the intersect timing
logic (the block that sets intersectStart, capturedTop, capturedScrollY) so
impressions only begin when entry.intersectionRatio meets the 50% threshold;
keep the else branch that calls fireImpressionEvent unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3ddbe45d-52e7-463a-9ac7-02b5d9f5b0c4

📥 Commits

Reviewing files that changed from the base of the PR and between 1cedf9b and 9eb0385.

📒 Files selected for processing (12)
  • .github/workflows/pr-auto-label.yml
  • frontend/.claude/commands/check-tracking.md
  • frontend/docs/claude/conventions.md
  • frontend/docs/features/main/club-card-tracking.md
  • frontend/middleware.ts
  • frontend/scripts/cross-review.sh
  • frontend/src/constants/eventName.ts
  • frontend/src/hooks/Mixpanel/useScrollTracking.ts
  • frontend/src/pages/MainPage/components/Banner/Banner.tsx
  • frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx
  • frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.tsx
  • frontend/src/utils/webviewBridge.ts
✅ Files skipped from review due to trivial changes (2)
  • frontend/docs/features/main/club-card-tracking.md
  • frontend/.claude/commands/check-tracking.md


steps:
- name: Auto Label PR
uses: actions/github-script@v7
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 워크플로에서 SHA 미고정 uses 탐지
rg -n '^\s*uses:\s*[^@]+@(v[0-9]+|main|master)$' .github/workflows

Repository: Moadong/moadong

Length of output: 3279


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,120p' .github/workflows/pr-auto-label.yml | nl -ba | sed -n '1,120p'

Repository: Moadong/moadong

Length of output: 102


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n .github/workflows/pr-auto-label.yml | sed -n '1,120p'

Repository: Moadong/moadong

Length of output: 3399


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "== pr-auto-label.yml: uses lines =="
rg -n '^\s*uses:\s*' .github/workflows/pr-auto-label.yml
echo
echo "== pr-auto-label.yml: full file (for small file) =="
wc -l .github/workflows/pr-auto-label.yml
cat .github/workflows/pr-auto-label.yml
echo
echo "== repo-wide: github-script `@v7` usages =="
rg -n 'actions/github-script@v[0-9]+' .github/workflows

Repository: Moadong/moadong

Length of output: 3165


🏁 Script executed:

#!/bin/bash
# 워크플로에서 SHA 미고정 uses 탐지
rg -n '^\s*uses:\s*[^@]+@(v[0-9]+|main|master)$' .github/workflows

Repository: Moadong/moadong

Length of output: 3279


🏁 Script executed:

#!/bin/bash
# 워크플로에서 SHA 미고정 uses 탐지
rg -n '^\s*uses:\s*[^@]+@(v[0-9]+|main|master)$' .github/workflows

Repository: Moadong/moadong

Length of output: 3279


🏁 Script executed:

#!/bin/bash
# 워크플로에서 SHA 미고정 uses 탐지
rg -n '^\s*uses:\s*[^@]+@(v[0-9]+|main|master)$' .github/workflows

Repository: Moadong/moadong

Length of output: 3279


🏁 Script executed:

#!/bin/bash
# 워크플로에서 SHA 미고정 uses 탐지
rg -n '^\s*uses:\s*[^@]+@(v[0-9]+|main|master)$' .github/workflows

Repository: Moadong/moadong

Length of output: 3279


🏁 Script executed:

#!/bin/bash
# 워크플로에서 SHA 미고정 uses 탐지
rg -n '^\s*uses:\s*[^@]+@(v[0-9]+|main|master)$' .github/workflows

Repository: Moadong/moadong

Length of output: 3279


actions/github-script@v7를 커밋 SHA로 고정하세요
.github/workflows/pr-auto-label.ymluses: actions/github-script@v7는 태그 참조라 업스트림 변경에 따라 동작이 달라질 수 있어 릴리즈 재현성이 떨어집니다. 커밋 SHA로 핀닝해 주세요.

🔒 제안 변경
-        uses: actions/github-script@v7
+        uses: actions/github-script@<FULL_LENGTH_COMMIT_SHA>
🧰 Tools
🪛 zizmor (1.25.2)

[error] 17-17: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pr-auto-label.yml at line 17, 현재 워크플로우에서 사용 중인
actions/github-script@v7 태그는 업스트림 변경에 따라 동작이 바뀔 수 있으니, 'uses:
actions/github-script@v7'를 해당 액션 리포지토리의 특정 커밋 SHA로 교체하여 핀닝하세요;
.github/workflows/pr-auto-label.yml에서 'uses' 항목을 찾아 actions/github-script 레포에서
v7 태그와 대응하는 최신 커밋 SHA로 바꾸고 커밋 SHA 문자열을 사용해 고정하면 됩니다.

Comment thread frontend/middleware.ts
Comment on lines +86 to +91
{
headers: {
'content-type': 'text/html; charset=utf-8',
'cache-control': 'public, s-maxage=300, stale-while-revalidate=60',
},
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 목적: 캐시 헤더에 Vary(User-Agent) 누락 여부를 정적으로 확인
cat -n frontend/middleware.ts | sed -n '84,92p'
rg -n "'cache-control'|'vary'" frontend/middleware.ts

Repository: Moadong/moadong

Length of output: 450


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) 해당 파일 전체에서 vary/Vary 설정 여부 확인
rg -n "(^|[^A-Za-z])Vary([^A-Za-z]|$)|['\"]vary['\"]|VARY" frontend/middleware.ts || true

# 2) 문제로 지목된 응답 헤더 블록(라인 84~92) 주변 로직/조건(UA 체크 등) 확인
cat -n frontend/middleware.ts | sed -n '60,120p'

Repository: Moadong/moadong

Length of output: 1508


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# UA(봇) 조건이 어디서 걸리는지 확인
rg -n "(user-?agent|bot|crawler|crawl|spider|isBot|ua)" frontend/middleware.ts

# OG HTML 응답을 만드는 Response 블록(헤더) 전체 중 vary/Vary 포함 여부 추가 확인
rg -n "new Response\\(|headers:\\s*\\{|cache-control" frontend/middleware.ts -n

Repository: Moadong/moadong

Length of output: 383


🏁 Script executed:

#!/bin/bash
set -euo pipefail
wc -l frontend/middleware.ts
rg -n "(^|[^A-Za-z])Vary([^A-Za-z]|$)|['\"]vary['\"]|VARY" frontend/middleware.ts || true
cat -n frontend/middleware.ts | sed -n '1,110p'

Repository: Moadong/moadong

Length of output: 3570


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n frontend/middleware.ts | sed -n '80,95p'
rg -n "'cache-control'|'vary'" frontend/middleware.ts

Repository: Moadong/moadong

Length of output: 727


미들웨어 OG HTML 응답에 Vary: user-agent를 추가하세요.

frontend/middleware.tsCRAWLER_PATTERN에 매칭되는 봇 UA일 때만 OG HTML을 cache-control: public으로 반환하지만, 해당 응답에 Vary가 없어 shared cache에 저장된 봇용 HTML이 동일 URL의 다른 UA 요청에도 재사용될 수 있습니다.

수정 예시
       {
         headers: {
           'content-type': 'text/html; charset=utf-8',
+          vary: 'user-agent',
           'cache-control': 'public, s-maxage=300, stale-while-revalidate=60',
         },
       },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
headers: {
'content-type': 'text/html; charset=utf-8',
'cache-control': 'public, s-maxage=300, stale-while-revalidate=60',
},
},
{
headers: {
'content-type': 'text/html; charset=utf-8',
vary: 'user-agent',
'cache-control': 'public, s-maxage=300, stale-while-revalidate=60',
},
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/middleware.ts` around lines 86 - 91, 응답 헤더 블록(현재 headers 객체)에 Vary:
user-agent를 추가하여 CRAWLER_PATTERN에 매칭되는 봇용 OG HTML을 반환할 때 공유 캐시가 다른 User-Agent에
대해 봇 HTML을 재사용하지 않도록 하세요; middleware.ts의 CRAWLER_PATTERN 판정 로직이 OG HTML을 선택하는
분기(및 해당 Response 생성 시 사용하는 headers 객체)를 찾아 'Vary': 'user-agent' 헤더를 병합해 포함하도록
수정하면 됩니다.

Comment on lines 45 to +59
const handlePrev = () => {
swiperInstance?.slidePrev();
trackEvent(USER_EVENT.BANNER_NAVIGATION_CLICKED, {
direction: 'prev',
from_index: currentIndex,
});
};

const handleNext = () => {
swiperInstance?.slideNext();
trackEvent(USER_EVENT.BANNER_NAVIGATION_CLICKED, {
direction: 'next',
from_index: currentIndex,
});
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Swiper 인스턴스가 없을 때 네비게이션 이벤트가 오탐될 수 있습니다.

현재는 swiperInstancenull이어도 BANNER_NAVIGATION_CLICKED가 전송됩니다. 실제 슬라이드 이동이 발생했을 때만 트래킹되도록 가드해 주세요.

🎯 제안 변경
  const handlePrev = () => {
-   swiperInstance?.slidePrev();
+   if (!swiperInstance) return;
+   swiperInstance.slidePrev();
    trackEvent(USER_EVENT.BANNER_NAVIGATION_CLICKED, {
      direction: 'prev',
      from_index: currentIndex,
    });
  };

  const handleNext = () => {
-   swiperInstance?.slideNext();
+   if (!swiperInstance) return;
+   swiperInstance.slideNext();
    trackEvent(USER_EVENT.BANNER_NAVIGATION_CLICKED, {
      direction: 'next',
      from_index: currentIndex,
    });
  };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/MainPage/components/Banner/Banner.tsx` around lines 45 -
59, The tracking fires even when swiperInstance is null; update handlePrev and
handleNext to only call trackEvent after confirming the swiper instance exists
and the navigation method was invoked: check swiperInstance (and optional method
existence) before calling swipe actions (e.g., in handlePrev verify
swiperInstance && swiperInstance.slidePrev()) and then call
trackEvent(USER_EVENT.BANNER_NAVIGATION_CLICKED, { direction: 'prev'|'next',
from_index: currentIndex }) inside that guarded branch so events are only sent
when the slide navigation actually occurred.

Comment on lines +77 to +91
if (entry.isIntersecting) {
if (intersectStart !== null) return;
const lastTime = parseInt(
sessionStorage.getItem(SS_LAST_KEY) ?? '0',
10,
);
if (Date.now() - lastTime < COOLDOWN_MS) return;
intersectStart = Date.now();
capturedTop = Math.round(entry.boundingClientRect.top);
capturedScrollY = Math.round(window.scrollY);
} else {
fireImpressionEvent();
}
},
{ threshold: IMPRESSION_THRESHOLD },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

IntersectionObserverEntry에서 isIntersecting과 intersectionRatio의 의미 차이, 그리고 threshold=0.5일 때 권장 판정 방식은 무엇인가요?

💡 Result:

결론부터 말하면: - isIntersecting: “타겟이 root와 교차(intersect) 상태냐(참/거짓)”를 나타내는 불리언이며, 0-면적(edge-adjacent 등) 같은 예외 케이스에서도 transition 구분이 가능하도록 정의돼 있습니다. [1][2] - intersectionRatio: “root 안에서 실제로 보이는 면적 비율”을 0.01.0의 숫자로 나타냅니다(면적이 0이면 isIntersecting에 따라 1 또는 0이 됩니다). [3][1] - threshold=0.5에서 “권장 판정 방식”은 보통 (1) isIntersecting으로 transition이 들어오는지 1차 필터링하고, (2) 실제로는 intersectionRatio >= 0.5로 임계치 충족을 판정하는 방식입니다(그래야 threshold 의미와 정확히 맞습니다). [4][3] 1) isIntersecting 의미(차이점의 핵심) - isIntersecting는 “타겟이 root와 intersects 했는가”를 Boolean으로 알려줍니다. [2] - 스펙 정의상, targetRect와 rootBounds가 교차하거나(edge-adjacent 포함) zero-area 교차인 경우에도 isIntersecting을 true로 둘 수 있어, “교차 상태로 들어오는 transition”과 “나가는 transition”을 구분하기 위한 플래그로 설명됩니다. [1][5] - 또한 isIntersecting은 thresholds/threshold-crossing 알림과는 독립적으로, “현재 교차 상태 여부” 성격이 강합니다(다만 콜백이 발행되는 타이밍은 threshold crossing에 의해 결정됩니다). [4][5] 2) intersectionRatio 의미(차이점의 핵심) - intersectionRatio는 타겟이 root 안에서 얼마나 보이는지(면적 기준)를 0.01.0 숫자로 나타냅니다. [3] - 구체적으로 boundingClientRect(타겟 bounds)의 면적이 0이 아니라면 intersectionRect 면적 / boundingClientRect 면적입니다. [1] - boundingClientRect 면적이 0인 경우에는, isIntersecting이 true면 1, 아니면 0으로 정의됩니다. [1][3] 3) threshold=0.5일 때 “권장 판정 방식” - IntersectionObserver는 지정한 thresholds 값을 “교차 비율이 threshold을 넘는지” 변화가 있을 때 콜백을 호출하도록 설계돼 있습니다. [6][1] - 그래서 threshold=0.5를 의미 있게 쓰려면, 콜백 진입 후의 판정은 intersectionRatio >= 0.5로 하는 것이 가장 직관적/정확합니다(입/출 중간 영역에서의 오차나 edge-case를 줄임). [3] - 실무적으로 MDN 예시에서도 isIntersecting로 transition(교차로 들어옴) 여부를 1차 확인한 뒤, intersectionRatio가 특정 값 이상인지로 최종 판정을 합니다. [4] 권장 패턴(예시): const observer = new IntersectionObserver((entries) => { for (const entry of entries) { // 1) 교차 transition 관점 1차 필터 if (!entry.isIntersecting) continue; // 2) threshold=0.5의 의미를 정확히 반영하는 최종 판정 if (entry.intersectionRatio >= 0.5) { // 0.5 이상 보일 때 처리 } } }, { threshold: 0.5 }); 이 패턴이 특히 좋은 이유는: - isIntersecting는 “교차 상태” 불리언(0-면적/edge-adjacent도 포함해 transition 구분 목적)이고 [1][2] - intersectionRatio는 “보이는 비율(면적 기반)”을 수치로 주기 때문에 [3][1] - threshold=0.5의 ‘의미(50% 비율)’는 intersectionRatio로 최종 확인하는 편이 specification의 정의와 맞습니다. [3][1] 참고: threshold를 0.5처럼 0이 아닌 값으로 주면, 콜백은 “ratio가 threshold을 넘나드는 순간”에 주로 오고, isIntersecting만을 단순히 “뷰포트 안이다/밖이다”처럼 해석하면 기대와 다를 수 있습니다(콜백 트리거 조건이 threshold crossing이기 때문). [1][7][5]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx"

# Confirm file exists and print the relevant section with line numbers
if [ -f "$FILE" ]; then
  sed -n '60,120p' "$FILE" | nl -ba -w3 -s': ' | sed -n '1,120p'
  echo "---- constants nearby ----"
  rg -n "IMPRESSION_THRESHOLD|COOLDOWN_MS|SS_LAST_KEY|threshold" "$FILE" || true
else
  echo "File not found: $FILE"
  git ls-files | rg -n "ClubCard\.tsx$" || true
fi

Repository: Moadong/moadong

Length of output: 102


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx"

if [ ! -f "$FILE" ]; then
  echo "File not found: $FILE"
  exit 0
fi

echo "---- snippet (lines 60-120) ----"
awk 'NR>=60 && NR<=120 {printf "%d:%s\n", NR, $0}' "$FILE"

echo "---- grep constants/usages ----"
rg -n "IMPRESSION_THRESHOLD|COOLDOWN_MS|SS_LAST_KEY|threshold|intersectionRatio|isIntersecting" "$FILE" || true

Repository: Moadong/moadong

Length of output: 2709


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx"

awk 'NR>=1 && NR<=120 {printf "%d:%s\n", NR, $0}' "$FILE"

Repository: Moadong/moadong

Length of output: 4455


50% 노출 기준을 entry.intersectionRatio로 최종 게이트하세요.

현재는 IntersectionObserver{ threshold: IMPRESSION_THRESHOLD }를 줬지만, 실제 분기는 entry.isIntersecting만 사용하고 있어(50% 비율 판정이 아님) “50% 이상 노출”보다 느슨하게 impression이 집계될 수 있습니다.

📏 제안 변경
-        if (entry.isIntersecting) {
+        const inView = entry.intersectionRatio >= IMPRESSION_THRESHOLD;
+        if (inView) {
           if (intersectStart !== null) return;
           const lastTime = parseInt(
             sessionStorage.getItem(SS_LAST_KEY) ?? '0',
             10,
           );
           if (Date.now() - lastTime < COOLDOWN_MS) return;
           intersectStart = Date.now();
           capturedTop = Math.round(entry.boundingClientRect.top);
           capturedScrollY = Math.round(window.scrollY);
         } else {
           fireImpressionEvent();
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx` around lines 77
- 91, The intersection callback currently gates impressions using
entry.isIntersecting, which ignores the intended IMPRESSION_THRESHOLD; change
the condition to check entry.intersectionRatio >= IMPRESSION_THRESHOLD (in
addition to or instead of isIntersecting) before starting the intersect timing
logic (the block that sets intersectStart, capturedTop, capturedScrollY) so
impressions only begin when entry.intersectionRatio meets the 50% threshold;
keep the else branch that calls fireImpressionEvent unchanged.

@suhyun113 suhyun113 merged commit b1d70d6 into main May 22, 2026
7 checks passed
@seongwon030 seongwon030 changed the title Develop fe [release] FE v1.7.4 May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🚁AI claude, codex 활용 시 💻 FE Frontend 🔧 PATCH Patch 릴리즈 📈 release 릴리즈 배포

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants