[feature] 상세 페이지 모바일 탑바 웹뷰로 이관 및 리액트 네이티브 API 연동 구조 도입#1090
[feature] 상세 페이지 모바일 탑바 웹뷰로 이관 및 리액트 네이티브 API 연동 구조 도입#1090oesnuj merged 26 commits intodevelop-fefrom
Conversation
- Vite 환경에서 SVG를 React 컴포넌트로 바로 import(`*.svg?react`)할 수 있도록 `vite-plugin-svgr` 플러그인 설치 및 설정 - TypeScript가 확장자를 인식할 수 있도록 타입 정의(`vite-env.d.ts`) 추가
- 앱 환경(`isInApp`)에서만 알림 버튼이 표시되도록 처리
- 앱 환경(React Native WebView)과 통신하기 위한 postMessage 래퍼 함수 구현 - 알림 구독/취소 및 뒤로가기 요청 기능
- 기존 헤더를 새로 구현된 MobileHeader로 교체 - 스크롤 인터랙션 및 탭 네비게이션 연동
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. Warning
|
| Cohort / File(s) | 변경 요약 |
|---|---|
Vite / SVGR 설정 frontend/config/vite.config.ts, frontend/package.json, frontend/src/vite-env.d.ts |
vite-plugin-svgr 의존성 추가 및 Vite 플러그인 등록, svgr용 ambient 타입 참조 추가 |
WebView 브리지 유틸 frontend/src/utils/webviewBridge.ts |
WebView↔네이티브 메시지 타입 정의 및 postMessageToApp과 페이사드 함수(requestNavigateBack, requestNotificationSubscribe, requestNotificationUnsubscribe) 추가 |
ClubDetail 탑바 컴포넌트 frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx, .../ClubDetailTopBar.styles.ts |
모바일/웹뷰 전용 상단바 컴포넌트 추가 (뒤로가기, 알림 토글, 탭바, 스크롤 기반 가시성/고정) |
ClubDetailPage 통합 변경 frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx, frontend/src/pages/ClubDetailPage/ClubDetailPage.styles.ts |
모바일/태블릿 감지에 따른 조건부 TopBar 렌더링, 탭 클릭을 URL 검색 파라미터로 통합(replace:true), 탭 전환 시 콘텐츠로 스크롤 정렬 로직 추가 |
프로필 카드: 뒤로가기 제거 frontend/src/pages/ClubDetailPage/components/ClubProfileCard/ClubProfileCard.tsx, .../ClubProfileCard.styles.ts |
BackButton 컴포넌트 및 관련 내비게이션 핸들러/스타일 제거, IntroDescription 모바일 폰트 오버라이드 제거 |
스크롤 훅 및 버튼 리팩토링 frontend/src/hooks/Scroll/useScrollTo.ts, frontend/src/hooks/Scroll/ScrollToTop.tsx, frontend/src/components/common/ScrollToTopButton/ScrollToTopButton.tsx |
useScrollTo 훅 추가 및 기존 직접 window.scroll 호출을 훅으로 대체 (scrollToTop, scrollToElement 등) |
스타일 조정 frontend/src/pages/ClubDetailPage/components/..., frontend/src/pages/ClubDetailPage/ClubDetailPage.styles.ts |
TabButton 높이(26px) 추가 및 일부 스타일/컴포넌트(export) 제거 |
Sequence Diagram(s)
sequenceDiagram
actor User
participant Page as ClubDetailPage
participant TopBar as ClubDetailTopBar
participant Bridge as WebViewBridge
participant Native as NativeApp
User->>TopBar: 뒤로가기 버튼 클릭
TopBar->>Bridge: requestNavigateBack()
Bridge->>Native: postMessage("{\"type\":\"NAVIGATE_BACK\"}")
alt In WebView
Native-->>Bridge: 처리/응답
Bridge-->>TopBar: true
else Browser
Bridge-->>TopBar: false
TopBar->>Page: react-router navigate(-1) (fallback)
end
sequenceDiagram
actor User
participant TopBar as ClubDetailTopBar
participant Bridge as WebViewBridge
participant Native as NativeApp
User->>TopBar: 알림 토글 클릭
TopBar->>Bridge: requestNotificationSubscribe/Unsubscribe(payload)
Bridge->>Native: postMessage("{\"type\":\"NOTIFICATION_*\",\"payload\":...}")
alt In WebView
Native-->>Bridge: 처리 결과
Bridge-->>TopBar: true
TopBar->>TopBar: isNotificationActive 업데이트
else Browser
Bridge-->>TopBar: false
end
sequenceDiagram
actor User
participant Page as ClubDetailPage
participant Scroll as useScrollTo
User->>Page: 탭 클릭
Page->>Page: 검색 파라미터 업데이트 (replace:true)
Page->>Scroll: scrollToElement(contentRef, offset=TOP_BAR_HEIGHT)
Scroll-->>Page: 스무스 스크롤 수행 (콘텐츠가 TopBar 아래로 보이도록)
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~60 분
Possibly related issues
- [feature] MOA-507 상세 페이지 모바일 탑바 웹뷰로 이관 및 네이티브 API 연동 구조 도입 #1027: 모바일 상세 탑바 및 네이티브 메시지 브리지 도입 목표와 직접 관련 — 동일 기능 영역을 구현함.
- [feature] WebView 메시지 핸들러 구현 - 알림 구독 및 네비게이션 처리 moadong-react-native#3: WebView가 수신할 NAVIGATE_BACK/NOTIFICATION_* 메시지 규격 도입과 직접 연관.
Possibly related PRs
- [feature] 상세페이지 소개 내용 및 FAQ 컴포넌트 구현 #959: ClubDetailPage 탑바/콘텐츠 관련 코드 레벨 겹침.
- [feature] 스크롤 트리거 훅과 스크롤 상단 이동 버튼 추가 #800: 스크롤 훅 및 ScrollToTopButton 관련 변경과 연관.
- [feature] 웹뷰 감지 로직 추가 및 웹뷰용 UI 조건부 처리 #828: 웹뷰 인식 기반 탑바/내비게이션 처리와 중복/연관됨.
Suggested reviewers
- lepitaaar
- suhyun113
- seongwon030
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title check | ✅ Passed | The PR title clearly describes the main feature: introducing a mobile top bar for the detail page via WebView and integrating React Native API communication structure, which aligns with the primary changes throughout the changeset. |
| Linked Issues check | ✅ Passed | The PR implements all major coding requirements from MOA-507: ClubDetailTopBar component with mobile UI [ClubDetailTopBar.tsx/.styles.ts], webviewBridge for native API communication [webviewBridge.ts], back navigation and notification subscription features, scroll-to utilities [useScrollTo.ts], and mobile environment detection integration. |
| Out of Scope Changes check | ✅ Passed | All changes are directly related to MOA-507 objectives: UI components (ClubDetailTopBar, style updates), WebView bridge utilities, scroll utilities supporting the new features, and SVGR configuration enabling icon assets. No unrelated changes detected. |
| Docstring Coverage | ✅ Passed | No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. |
✏️ Tip: You can configure your own custom pre-merge checks in the settings.
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands and usage tips.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@frontend/src/pages/ClubDetailPage/components/MobileHeader/MobileHeader.tsx`:
- Around line 33-34: Remove the stale TODO comment above the isInApp assignment
in MobileHeader.tsx: the comment says to restore isInAppWebView() on deploy but
the code already calls isInAppWebView(); delete the TODO line so only the
declaration const isInApp = isInAppWebView(); remains, leaving no misleading
comment in the MobileHeader component.
- Around line 56-64: The click handler only calls requestNotificationSubscribe
and never unsubscribes; update handleNotificationClick to perform a toggle:
check the current isNotificationActive state and call
requestNotificationUnsubscribe(clubId, clubName) when true or
requestNotificationSubscribe(clubId, clubName) when false, then update
setIsNotificationActive based on the returned success value from the invoked
function; ensure you reference the existing requestNotificationUnsubscribe and
requestNotificationSubscribe functions and only flip state when the call returns
success.
🧹 Nitpick comments (5)
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (1)
64-81: 탭 설정을 상수로 추출하는 것을 고려하세요.
tabs배열이 렌더링마다 새로 생성됩니다. 성능에 큰 영향은 없지만, 컴포넌트 외부에 상수로 추출하면 가독성과 재사용성이 향상됩니다.♻️ 제안된 리팩토링
+const DETAIL_TABS = [ + { key: TAB_TYPE.INTRO, label: '소개내용' }, + { key: TAB_TYPE.PHOTOS, label: '활동사진' }, +] as const; + const ClubDetailPage = () => { // ... {showMobileHeader && ( <MobileHeader clubId={clubId || ''} clubName={clubDetail.name} - tabs={[ - { key: TAB_TYPE.INTRO, label: '소개내용' }, - { key: TAB_TYPE.PHOTOS, label: '활동사진' }, - ]} + tabs={DETAIL_TABS} activeTab={activeTab} onTabClick={(tabKey) => {frontend/src/pages/ClubDetailPage/components/MobileHeader/MobileHeader.tsx (1)
35-35: 알림 구독 상태의 초기값 동기화가 필요할 수 있습니다.컴포넌트가 마운트될 때
isNotificationActive가 항상false로 시작합니다. 이미 구독한 사용자가 페이지를 다시 방문하면 버튼이 비활성 상태로 표시됩니다. 네이티브 앱에서 초기 구독 상태를 받아오는 메시지 핸들러 추가를 고려하세요.frontend/src/utils/webviewBridge.ts (1)
33-38:postMessage호출 후 성공 여부를 확인할 수 없습니다.
ReactNativeWebView?.postMessage가undefined일 경우 아무 일도 일어나지 않지만true를 반환합니다.isInAppWebView()가true를 반환했는데ReactNativeWebView가 없는 경우 문제를 감지할 수 없습니다.♻️ 명시적 검사 제안
try { + if (!window.ReactNativeWebView) { + if (isDev) { + console.warn('[WebViewBridge] ReactNativeWebView 없음'); + } + return false; + } - window.ReactNativeWebView?.postMessage(JSON.stringify(message)); + window.ReactNativeWebView.postMessage(JSON.stringify(message)); if (isDev) { console.log('[WebViewBridge] 앱으로 전송:', message.type); } return true;frontend/src/pages/ClubDetailPage/components/MobileHeader/MobileHeader.styles.ts (2)
14-21: 사용되지 않는$isVisibleprop
Header컴포넌트에$isVisibleprop이 타입으로 선언되어 있지만, 실제 스타일 정의에서 사용되지 않습니다. 불필요한 prop은 제거하거나, 의도된 스타일링이 있다면 구현해 주세요.♻️ 사용되지 않는 prop 제거 제안
-export const Header = styled.header<{ $isVisible: boolean }>` +export const Header = styled.header` position: relative; height: 60px; display: flex; align-items: center; justify-content: space-between; padding: 0 16px; `;
36-37: 하드코딩된rgba값 중복
IconButton과NotificationButton에서 동일한rgba(255, 255, 255, 0.8)값이 중복 사용되고 있습니다. 추후 유지보수를 위해 테마 토큰으로 추출하거나 상수로 정의하는 것을 고려해 주세요.Also applies to: 59-62
frontend/src/pages/ClubDetailPage/components/MobileHeader/MobileHeader.tsx
Outdated
Show resolved
Hide resolved
frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx
Show resolved
Hide resolved
- 스크롤 로직 공통화를 위한 useScrollTo 커스텀 훅 생성 - ScrollToTopButton 및 ClubDetailPage에 새로운 훅 적용 - 상세페이지 탭 전환 시 히스토리가 쌓이지 않도록 replace 옵션 적용하여 뒤로가기 동작 개선
…ttps://github.com/Moadong/moadong into feature/#1027-mobile-detail-webview-topbar-MOA-507
- 원본 탭 클릭 시 스크롤 이동 제거 - MobileHeader 탭바(sticky) 클릭 시에만 컨텐츠로 스크롤
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In
`@frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx`:
- Around line 92-96: NotificationIcon uses the same color as NotificationButton
when isNotificationActive is true, making the icon invisible; update the fill
logic in ClubDetailTopBar's NotificationIcon usage so when isNotificationActive
is true the icon uses a contrasting color (e.g., theme.colors.white or
theme.colors.primary[50]) instead of theme.colors.primary[900], otherwise keep
theme.colors.gray[500]; reference the NotificationIcon component, the
isNotificationActive flag and the NotificationButton styling to ensure
active-state contrast.
- Line 40: isNotificationActive이 항상 false로 초기화되어 구독된 사용자에게 잘못된 상태를 보여주므로
ClubDetailTopBar 컴포넌트의 마운트 시점에 실제 구독 상태를 받아와 상태를 동기화하세요; 구체적으로
ClubDetailTopBar에서 useEffect를 추가해 webviewBridge의 구독 상태 조회 API(예:
webviewBridge.getNotificationSubscriptionStatus 또는 새로 추가될 유사 함수)를 호출하고 그 결과로
setIsNotificationActive를 호출해 초기값을 설정하거나, 대신 부모 컴포넌트에서 isNotificationActive 초기값을
props로 전달받도록 ClubDetailTopBar의 인터페이스를 변경해 초기 상태를 반영하도록 하세요.
♻️ Duplicate comments (1)
frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx (1)
61-69: 알림 구독 해제 기능이 누락되었습니다.현재
handleNotificationClick은 구독만 처리하고, 이미 구독된 상태에서 클릭 시 구독 해제 기능이 없습니다.
🧹 Nitpick comments (2)
frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.styles.ts (1)
14-21:$isVisibleprop이 사용되지 않고 있습니다.
Header컴포넌트에서$isVisibleprop을 선언했지만 실제 스타일에서 사용하지 않습니다. 의도된 동작이라면 prop을 제거하고, 스타일 변경이 필요하다면 구현해 주세요.♻️ 미사용 prop 제거 제안
-export const Header = styled.header<{ $isVisible: boolean }>` +export const Header = styled.header` position: relative; height: 60px; display: flex; align-items: center; justify-content: space-between; padding: 0 16px; `;frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (1)
81-89: 타입 안전성을 위해tabs정의와onTabClick타입을 일치시키세요.
tabsprop에서key는TAB_TYPE상수를 사용하지만,ClubDetailTopBarProps의onTabClick은string을 받습니다. 부모에서tabKey as TabType캐스팅이 필요하게 되어 타입 안전성이 떨어집니다.♻️ 제네릭 또는 명시적 타입 사용 제안
ClubDetailTopBar에서TabItem과onTabClick타입을 일치시키거나, 부모에서 타입을 명시하세요:// ClubDetailTopBar.tsx에서 interface ClubDetailTopBarProps<T extends string = string> { // ... tabs?: { key: T; label: string }[]; activeTab?: T; onTabClick?: (tabKey: T) => void; }또는 현재 구조를 유지하면서
TAB_TYPE값들을 상수로 export하여ClubDetailTopBar에서도 재사용할 수 있습니다.
frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx
Outdated
Show resolved
Hide resolved
frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx`:
- Around line 54-57: The handleTabClick currently calls setSearchParams({ tab:
tabKey }, { replace: true }) which wipes existing query params (e.g.,
is_subscribed); change it to use the functional form of setSearchParams inside
handleTabClick (referencing handleTabClick and setSearchParams) so you
merge/update the existing URLSearchParams and only set/replace the tab key while
preserving other params, then call with { replace: true } as before.
🧹 Nitpick comments (1)
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (1)
25-25: 탑바 높이 하드코딩은 오프셋 불일치 리스크가 있습니다.
디자인/세이프에어리어 변경 시 스크롤 오프셋이 어긋날 수 있어, 실제 높이 측정 또는 CSS 변수로 관리하는 방식을 권장합니다.Also applies to: 46-52
소개내용/활동사진 탭 클릭 시 Sticky 탭바 하단에 콘텐츠가 정확히 위치하도록 높이 값을 65에서 50으로 조정
웹 모바일 환경에서 링크 등을 통해 상세 페이지로 직접 접근했을 때, 이전 히스토리가 없으면 메인 페이지로 이동하도록 뒤로가기 로직을 개선
| /// <reference types="vite/client" /> | ||
| /// <reference types="vite-plugin-svgr/client" /> |
There was a problem hiding this comment.
TypeScript가 *.svg?react import를 인식하기 위한 타입 정의 파일입니다.
없으면 svg 컴포넌트 import 시 TS2307 에러 발생합니다.
| export const postMessageToApp = (message: WebViewMessage): boolean => { | ||
| if (!isInAppWebView()) { | ||
| if (isDev) { | ||
| console.log('[WebViewBridge] 웹 환경, 메시지 무시:', message.type); | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| try { | ||
| window.ReactNativeWebView?.postMessage(JSON.stringify(message)); | ||
| if (isDev) { | ||
| console.log('[WebViewBridge] 앱으로 전송:', message.type); | ||
| } | ||
| return true; | ||
| } catch (error) { | ||
| console.error('[WebViewBridge] 전송 실패:', error); | ||
| return false; | ||
| } | ||
| }; |
There was a problem hiding this comment.
[동작 흐름]
- 웹뷰에서 requestNavigateBack() 등 호출
- postMessageToApp()이 isInAppWebView()로 앱 환경인지 체크
- 앱이면 window.ReactNativeWebView.postMessage()로 JSON 전송
- React Native 앱이 메시지 수신 → 네이티브 동작 실행 (뒤로가기, 알림 등)
좀 더 구체적인 원리는 노션 참고
https://www.notion.so/WebView-Bridge-2efaad23209680c79da0e58abcb4f191?source=copy_link
seongwon030
left a comment
There was a problem hiding this comment.
수고하셨습니다 문서를 잘 작성해주셔서 이해하기 쉬웠습니다
payload를 강제해야 하는 부분이 신기하기도 하고 그에 따른 Facade패턴이나 타입 정의도 흥미롭게 봤습니다 !
frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.styles.ts
Show resolved
Hide resolved
frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In
`@frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx`:
- Around line 55-66: handleBackClick currently reads the internal React Router
field window.history.state.idx (not public); replace this with a supported check
by either using window.history.length to determine if a back navigation is
possible or by reading router state via useLocation() (e.g., check
location.state) together with navigate and requestNavigateBack; update the logic
inside handleBackClick (and keep calling requestNavigateBack and navigate as
before) so it no longer references window.history.state.idx and instead uses the
chosen public API to decide between navigate(-1) and navigate('/', { replace:
true }).
🧹 Nitpick comments (1)
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (1)
40-41: 사용하지 않는 변수가 있습니다.
isLaptop과isDesktop이 구조 분해되었지만 사용되지 않습니다. 필요한 값만 추출하면 코드가 더 명확해집니다.♻️ 제안 수정안
- const { isMobile, isTablet, isLaptop, isDesktop } = useDevice(); + const { isMobile, isTablet } = useDevice();
frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx
Show resolved
Hide resolved
…feature/#1027-mobile-detail-webview-topbar-MOA-507
#️⃣ 연관된 이슈
📌 작업 개요
모바일 상세페이지 웹뷰 탑바 및 UI 개선
동아리 상세페이지의 모바일/웹뷰 환경에서 사용자 경험을 개선하기 위한 UI/UX 업데이트입니다.
리액트 네이티브와 함께 작업을 진행했습니다
리액트 네이티브 PR : Moadong/moadong-react-native#5
변경사항
1. 웹뷰 모바일 탑바 구현
새로운 컴포넌트:
ClubDetailTopBar주요 기능:
웹뷰 브릿지 유틸리티 추가
Facade 패턴 적용:
지원하는 메시지 타입:
NAVIGATE_BACK: 앱에 뒤로가기 요청NOTIFICATION_SUBSCRIBE: 알림 구독 요청NOTIFICATION_UNSUBSCRIBE: 알림 구독 해제sequenceDiagram participant Web as React(모바일웹) participant Bridge as WebviewBridge participant App as App(WebView) Note over Web, App: 알림 버튼 클릭 시 흐름 Web->>Bridge: 1. requestNotificationSubscribe() 호출 Bridge->>Bridge: 2. isInAppWebView() 환경 체크 alt ✅ 앱 환경 (In-App) Bridge->>App: 3. postMessage({ type: 'NOTIFICATION_SUBSCRIBE' }) 전송 Note right of Bridge: 성공 반환 (true) Bridge-->>Web: 4. return true Web->>Web: 5. 아이콘 활성화 (Color: primary) else ❌ 웹 브라우저 (Not In-App) Note left of Bridge: 메시지 전송 안 함 (무시) Bridge-->>Web: return false Web->>Web: UI 변경 없음 (Color: Gray 유지) end2. 상세페이지 UI 수정
프로필 카드 UI 개선
스크롤 유틸리티 훅 추가
추가 배경
기존에는 상단으로만 스크롤하는 훅만 존재했으나, 특정 위치로 이동시키는 기능이 필요해 확장성을 고려하여 범용 스크롤 훅을 추가함.
활용 사례
기능:
scrollToTop(): 페이지 최상단으로 스크롤scrollToElement(): 특정 엘리먼트로 오프셋 포함 스크롤scrollTo(): 커스텀 위치로 스크롤3. 인프라/설정 변경
SVGR 플러그인 설정
/// <reference types="vite-plugin-svgr/client" />모바일 아이콘 에셋 추가
prev_button_icon.svgnext_button_icon.svgnotification_icon.svg🐛 트러블슈팅 (Fixes)
fixfixfixfixfixTOP_BAR_HEIGHT값 조정fix✅ 테스트 체크리스트
결과 화면
웹 모바일의 상세페이지
default.mp4
앱의 상세페이지
2026-01-24.143236.mp4
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선사항
✏️ Tip: You can customize this high-level summary in your review settings.