[release] FE v1.7.4#1560
Conversation
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 옵션으로 이식성 보장
…ttps://github.com/Moadong/moadong into feature/#1545-cross-review-and-jira-story-MOA-854
…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): 웹뷰 메인페이지 좌우 스크롤 방지 및 렌더링 개선
There was a problem hiding this comment.
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 win6번째 매개변수(빈 문자열)의 용도를 문서화하세요.
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
⛔ Files ignored due to path filters (1)
frontend/src/assets/images/icons/location_icon.svgis excluded by!**/*.svg
📒 Files selected for processing (36)
frontend/.claude/agents/RN에이전트.mdfrontend/.claude/commands/RN.mdfrontend/.claude/commands/check-tracking.mdfrontend/.claude/commands/jira-story.mdfrontend/.claude/commands/jira-task.mdfrontend/.claude/commands/mixpanel-ab.mdfrontend/.claude/commands/mixpanel.mdfrontend/.claude/commands/prd.mdfrontend/.gitignorefrontend/docs/claude/api.mdfrontend/docs/claude/features.mdfrontend/docs/features/club-detail/map-event-tracking.mdfrontend/docs/features/event/card-heatmap-tracking.mdfrontend/docs/superpowers/specs/2026-04-14-design-system-toolkit-design.mdfrontend/scripts/cross-review.shfrontend/scripts/jira-story.shfrontend/scripts/jira-task.shfrontend/src/components/common/Header/Header.tsxfrontend/src/constants/eventName.tsfrontend/src/hooks/Header/useHeaderNavigation.tsfrontend/src/hooks/Mixpanel/useMixpanelTrack.tsfrontend/src/hooks/Mixpanel/useScrollTracking.tsfrontend/src/pages/ClubDetailPage/ClubDetailPage.tsxfrontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsxfrontend/src/pages/ClubDetailPage/components/ClubProfileCard/ClubProfileCard.styles.tsfrontend/src/pages/ClubDetailPage/components/ClubProfileCard/ClubProfileCard.tsxfrontend/src/pages/IntroducePage/components/sections/1.IntroSection/IntroSection.tsxfrontend/src/pages/MainPage/MainPage.tsxfrontend/src/pages/MainPage/components/Banner/Banner.tsxfrontend/src/pages/MainPage/components/ClubCard/ClubCard.tsxfrontend/src/pages/MainPage/components/SearchBox/SearchBox.tsxfrontend/src/pages/PromotionPage/components/detail/PromotionDetailTopBar/PromotionDetailTopBar.tsxfrontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.styles.tsfrontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.tsxfrontend/src/pages/WebviewMainPage/WebviewMainPage.tsxfrontend/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
- 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 리뷰 전용 서브커맨드 사용으로 저장소 수정 방지
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 웹뷰 네비게이션 동작 수정
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 트래킹 전면 개편 및 배너 네비게이션 트래킹 추가
[fix] ab-test 커맨드 절대경로로 변경
|
|
||
| <Styled.SocialLinksWrapper> | ||
| {getActiveSocials().map(({ platform, url }) => { | ||
| const icon = getSocialIcon(platform); |
There was a problem hiding this comment.
현재 인스타그램과 유튜브 링크만 보이도록 되어있는데, 관리자 페이지에는 X 링크도 있어서 개선해야겠네요.
| image: club.cover || club.logo || DEFAULT_OG_IMAGE, | ||
| url: `${SITE_URL}${safeDecode(pathname)}`, | ||
| }), | ||
| { headers: { 'content-type': 'text/html; charset=utf-8' } }, |
There was a problem hiding this comment.
OG 미들웨어가 크롤러 요청마다 매번 백엔드 API를 호출하고 있습니다. 같은 동아리 링크가 카카오톡 채팅방에서 여러 번 공유될 경우 동일한 데이터를 중복을 fetch하는 문제가 있을 것 같습니다
응답에 cache-control: public, s-maxage=300, stale-while-revalidate=60 헤더를 추가했습니다 9eb0385
Vercel Edge CDN이 OG 응답을 5분간 캐싱하도록 수정하여 같은 URL에 대한 백엔드 API 호출을 1회로 줄이고자 했습니다! 5분 만료 이후에도 60초간은 기존 응답을 내려주면서 백그라운드에서 갱신하도록 하여 크롤러가 빈 응답을 받을 확률은 감소시켰습니다!
There was a problem hiding this comment.
좋은 방법이네요 ! 먼가 활발하게 수정되는 시기와 아닌 시기에 따라 캐싱 시간을 다르게 두는 방법도 있을 것 같은데
나중에 한 번 얘기해보시죠
|
릴리즈 하겠습니당 |
There was a problem hiding this comment.
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
📒 Files selected for processing (12)
.github/workflows/pr-auto-label.ymlfrontend/.claude/commands/check-tracking.mdfrontend/docs/claude/conventions.mdfrontend/docs/features/main/club-card-tracking.mdfrontend/middleware.tsfrontend/scripts/cross-review.shfrontend/src/constants/eventName.tsfrontend/src/hooks/Mixpanel/useScrollTracking.tsfrontend/src/pages/MainPage/components/Banner/Banner.tsxfrontend/src/pages/MainPage/components/ClubCard/ClubCard.tsxfrontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.tsxfrontend/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 |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 워크플로에서 SHA 미고정 uses 탐지
rg -n '^\s*uses:\s*[^@]+@(v[0-9]+|main|master)$' .github/workflowsRepository: 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/workflowsRepository: Moadong/moadong
Length of output: 3165
🏁 Script executed:
#!/bin/bash
# 워크플로에서 SHA 미고정 uses 탐지
rg -n '^\s*uses:\s*[^@]+@(v[0-9]+|main|master)$' .github/workflowsRepository: Moadong/moadong
Length of output: 3279
🏁 Script executed:
#!/bin/bash
# 워크플로에서 SHA 미고정 uses 탐지
rg -n '^\s*uses:\s*[^@]+@(v[0-9]+|main|master)$' .github/workflowsRepository: Moadong/moadong
Length of output: 3279
🏁 Script executed:
#!/bin/bash
# 워크플로에서 SHA 미고정 uses 탐지
rg -n '^\s*uses:\s*[^@]+@(v[0-9]+|main|master)$' .github/workflowsRepository: Moadong/moadong
Length of output: 3279
🏁 Script executed:
#!/bin/bash
# 워크플로에서 SHA 미고정 uses 탐지
rg -n '^\s*uses:\s*[^@]+@(v[0-9]+|main|master)$' .github/workflowsRepository: Moadong/moadong
Length of output: 3279
🏁 Script executed:
#!/bin/bash
# 워크플로에서 SHA 미고정 uses 탐지
rg -n '^\s*uses:\s*[^@]+@(v[0-9]+|main|master)$' .github/workflowsRepository: Moadong/moadong
Length of output: 3279
actions/github-script@v7를 커밋 SHA로 고정하세요
.github/workflows/pr-auto-label.yml의 uses: 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 문자열을 사용해 고정하면 됩니다.
| { | ||
| headers: { | ||
| 'content-type': 'text/html; charset=utf-8', | ||
| 'cache-control': 'public, s-maxage=300, stale-while-revalidate=60', | ||
| }, | ||
| }, |
There was a problem hiding this comment.
🧩 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.tsRepository: 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 -nRepository: 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.tsRepository: Moadong/moadong
Length of output: 727
미들웨어 OG HTML 응답에 Vary: user-agent를 추가하세요.
frontend/middleware.ts는 CRAWLER_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.
| { | |
| 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' 헤더를 병합해 포함하도록
수정하면 됩니다.
| 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, | ||
| }); | ||
| }; |
There was a problem hiding this comment.
Swiper 인스턴스가 없을 때 네비게이션 이벤트가 오탐될 수 있습니다.
현재는 swiperInstance가 null이어도 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.
| 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 }, |
There was a problem hiding this comment.
🧩 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:
- 1: https://w3c.github.io/IntersectionObserver/
- 2: https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry/isIntersecting
- 3: https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry/intersectionRatio
- 4: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
- 5: https://www.w3.org/TR/2019/WD-intersection-observer-20190531/
- 6: https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver
- 7: https://github.com/WICG/IntersectionObserver/blob/master/explainer.md
🏁 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
fiRepository: 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" || trueRepository: 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.
🚀 릴리즈 PR
📦 버전 정보
💻 FE🔧 PATCH📖 버전 라벨 선택 가이드 (Semantic Versioning)
🚨 MAJORv1.0.0→v2.0.0➕ MINORv1.0.0→v1.1.0🔧 PATCHv1.0.0→v1.0.1📋 포함된 변경사항
✨ 신규 기능
@clubName형식·한글 URL 디코딩·3초 타임아웃 포함📊 트래킹 강화
dwell_ms) 측정, 300ms 미만 fly-by 필터링,view_count·reentry_count세션 집계useScrollTracking훅)from_index기록🔧 개선
useBanner24h→1h (긴급 배너 교체 대응),usePromotion5min→1min (refetchInterval 기준 정렬)🐛 버그 수정
PromotionClubCTA웹뷰 네비게이션@clubName기반으로 통일, bridge 실패 시 fallback 추가🛠 개발 도구
/cross-review(크로스 에이전트 코드 리뷰),/jira-story,/jira-task(Jira 스토리·하위작업 자동 생성),/prd(PRD 기반 하위작업 일괄 생성),/check-tracking(트래킹 감사),/RN(RN 에이전트 위임)Summary by CodeRabbit
릴리스 노트
New Features
Improvements
Bug Fixes