-
Notifications
You must be signed in to change notification settings - Fork 3
[feature] 웹/웹뷰 통합 + 웹 바텀 네비게이션 마이그레이션 (MOA-942) #1643
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
seongwon030
merged 19 commits into
develop-fe
from
feature/#1642-webview-web-migration-MOA-942
Jun 8, 2026
Merged
Changes from 12 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
49118d7
feat(components): 웹 바텀 네비게이션 컴포넌트 추가
seongwon030 3804dbf
fix(storybook): svg?react import 지원 추가
seongwon030 e5314c6
refactor(components): 바텀네비 아이콘 CSS mask → svg?react 전환
seongwon030 075f614
feat: 메뉴/구독 페이지 추가
seongwon030 26363a4
feat(layout): AppLayout 도입해 핵심 네비 페이지에 바텀네비 마운트
seongwon030 49f1c57
refactor(header): 헤더를 로고+검색만으로 간소화
seongwon030 114cfd1
refactor(webview): 웹뷰를 웹 라우트로 통합 (/webview 리다이렉트화)
seongwon030 f791af5
fix(header): 검색창 반응형 정렬 (데스크톱 우측, 태블릿 이하 꽉 채움)
seongwon030 309c444
refactor(header): 소개/연합회 페이지 웹뷰에서도 통일 헤더 노출
seongwon030 5fa1a7e
refactor(webview): 홍보목록 웹뷰 분기 제거 + Filter 헤더 여백 정렬
seongwon030 2379e16
docs(features): 웹/웹뷰 통합 라우팅으로 WebView 섹션 갱신
seongwon030 bab73de
fix: lint error
seongwon030 cbd63d9
fix(bottom-nav): 탭 활성 매핑 보정 + 데스크톱 폭 제한
seongwon030 ec6fd0e
fix(style): 스크롤바 거터 고정으로 바텀네비 레이아웃 시프트 제거
seongwon030 c1fdf73
feat(menu): 앱 버전 표시 추가
seongwon030 d489a6d
fix(menu): 앱 버전 UI를 메뉴 행 형태로 변경
seongwon030 8dff3d8
feat(layout): 바텀네비 모바일 전용으로 제한 + 데스크톱 헤더 nav 복원
seongwon030 9888dad
fix(review): PR #1643 리뷰 반영
seongwon030 c17a503
fix: lint error
seongwon030 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| # 웹 바텀 네비게이션 (BottomNavigation) | ||
|
|
||
| 앱 네이티브 바텀탭(React Navigation)을 웹으로 이식한 하단 고정 네비게이션. 웹/웹뷰 통합 마이그레이션의 일부로, RN `app/(tabs)/_layout.tsx` 스펙을 그대로 재현했다. | ||
|
|
||
| ## 탭 구성 | ||
|
|
||
| | 탭 | 경로 | 아이콘 | | ||
| | ---- | ---------------- | --------------------------------------------- | | ||
| | 홈 | `/` | home.svg (mask 틴팅) | | ||
| | 구독 | `/subscriptions` | subscribe_selected / _unselected.png (2-state) | | ||
| | 메뉴 | `/menu` | menu.svg (mask 틴팅) | | ||
|
|
||
| active 판정: `/`는 정확히 일치, 나머지는 `startsWith`. | ||
|
|
||
| ## 스타일 (RN 스펙 1:1) | ||
|
|
||
| - 컨테이너: 배경 `#FFFFFF`, 상단 border `1px #F0F0F0`, `position: fixed; bottom: 0` | ||
| - 패딩: top `6px`, bottom `calc(6px + env(safe-area-inset-bottom))` | ||
| - 아이콘 28×28, active `#FF5414`(primary[900]) / inactive `#C5C5C5`(gray[500]) | ||
| - 라벨 10px / weight 500 / 아이콘↔라벨 간격 4px | ||
| - z-index: `Z_INDEX.bottomNav` | ||
|
|
||
| ## 아이콘 틴팅 | ||
|
|
||
| 단색 아이콘(홈/메뉴)은 `svg?react`(vite-plugin-svgr)로 컴포넌트 import하고, RN 원본이 `currentColor`를 쓰므로 부모 `Tab`의 `color`(active `#FF5414` / inactive `#C5C5C5`)를 상속받아 틴팅된다. 라벨도 `color: inherit`로 같은 색을 따른다. RN에서 가져온 SVG의 배경 `<rect fill="white"/>`는 제거했다(아이콘 패스는 원본 유지). 구독 아이콘은 PNG 2-state라 선택/비선택 이미지를 교체한다. | ||
|
|
||
| ## 트래킹 | ||
|
|
||
| 탭 클릭 시 `USER_EVENT.BOTTOM_TAB_CLICKED`(`'BottomTab Clicked'`, 앱과 동일 문자열)를 전송한다 — payload `{ tab, path }`. | ||
|
|
||
| ## 관련 코드 | ||
|
|
||
| - `src/components/common/BottomNavigation/BottomNavigation.tsx` — 컴포넌트 | ||
| - `src/components/common/BottomNavigation/BottomNavigation.styles.ts` — 스타일 | ||
| - `src/components/common/BottomNavigation/BottomNavigation.stories.tsx` — Storybook 미리보기 | ||
| - `src/assets/images/icons/bottomNav/` — 아이콘 에셋 (RN에서 이식) | ||
| - `src/styles/zIndex.ts` — `bottomNav` z-index 토큰 | ||
| - `src/constants/eventName.ts` — `BOTTOM_TAB_CLICKED` | ||
|
|
||
| ## 마운트 | ||
|
|
||
| `src/layouts/AppLayout.tsx`(중첩 라우트 레이아웃)에서 렌더되며, `/`·`/introduce`·`/club-union`·`/promotions`·`/subscriptions`·`/menu` 6개 라우트에 노출된다. 상세·지원폼·관리자·게임·웹뷰 등은 그룹 밖이라 미노출. AppLayout이 `Outlet` 래퍼에 하단 여백(`calc(56px + env(safe-area-inset-bottom))`)을 주어 fixed 바에 콘텐츠가 가리지 않게 한다. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # 메뉴(더보기) 페이지 (MenuPage) | ||
|
|
||
| 바텀 네비게이션 "메뉴" 탭의 페이지(`/menu`). 앱 네이티브 더보기 화면(RN `app/(tabs)/more.tsx`)을 웹으로 이식했다. 통합 후 헤더가 로고+검색만 남으므로, 기존 헤더 메뉴 성격의 항목을 이 페이지로 모은다. | ||
|
|
||
| ## 항목 (RN 더보기 그대로) | ||
|
|
||
| | 항목 | 이동 | | ||
| | --- | --- | | ||
| | 서비스 소개 | `/introduce` | | ||
| | 총 동아리 연합회 | `/club-union` | | ||
| | 개인정보 처리방침 | 외부 노션 링크 (새 탭) | | ||
|
|
||
| RN의 "앱 버전" 항목은 웹에 해당 없어 제외. introduce/club-union은 기존 `useHeaderNavigation` 핸들러를 재사용해 네비게이션·Mixpanel 트래킹을 유지한다. 개인정보 처리방침은 Footer와 동일한 외부 링크를 사용한다. | ||
|
|
||
| ## 스타일 (RN 레이아웃 재현) | ||
|
|
||
| - "더보기" 타이틀 헤더 + 항목 리스트(행마다 border-bottom) | ||
| - 각 행: 원형 아이콘 컨테이너(`#FFECE5` 배경 + `#FF5414` 아이콘) + 제목 + chevron(`#C5C5C5`) | ||
| - 앞쪽 아이콘은 RN의 Ionicons가 웹에 없어 웹용 단순 SVG로 재현(`src/assets/images/icons/menu/`). 디자인 시안 확보 시 교체. | ||
|
|
||
| ## 관련 코드 | ||
|
|
||
| - `src/pages/MenuPage/MenuPage.tsx` — 페이지 | ||
| - `src/pages/MenuPage/MenuPage.styles.ts` | ||
| - `src/assets/images/icons/menu/` — info/people/document/chevron SVG (`?react`, currentColor 틴팅) | ||
| - `src/hooks/Header/useHeaderNavigation.ts` — 소개/연합회 네비 핸들러 (재사용) | ||
| - `src/routes/AppRoutes.tsx` — `/menu` 라우트 | ||
|
|
||
| ## 참고 | ||
|
|
||
| 아직 공용 레이아웃(헤더/바텀네비)에 부착되지 않은 독립 페이지다. 부착은 레이아웃 셸(AppLayout) 단계에서 진행한다. | ||
24 changes: 24 additions & 0 deletions
24
frontend/docs/features/subscriptions/subscriptions-page.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # 구독한 동아리 페이지 (SubscriptionsPage) | ||
|
|
||
| 바텀 네비게이션 "구독" 탭의 페이지(`/subscriptions`). 앱 네이티브 구독 화면(RN `ui/subscribe/`)을 웹으로 이식했다. | ||
|
|
||
| ## 동작 | ||
|
|
||
| - **웹뷰(`isInAppWebView()`)**: `useWebviewSubscribe`로 네이티브 브릿지에서 구독 동아리 ID(`subscribedClubIds`)를 받아오고, `useGetCardList`(메인과 동일 쿼리, 캐시 공유) 결과를 해당 ID로 필터링해 `ClubCard` + `SubscribeButton`(현장 구독 토글)로 렌더. 로딩/에러/빈 상태("홈으로 가기") 처리. | ||
| - **순수 웹(앱 아님)**: 구독은 네이티브 브릿지 전용이라 웹엔 데이터가 없다. "구독 기능은 모아동 앱에서 이용 가능" 안내 + 앱 다운로드 CTA(`getAppStoreLink`)를 렌더. | ||
|
|
||
| 구독 토글·상태 동기화는 기존 브릿지(`SUBSCRIBE_TOGGLE` / `SUBSCRIBE_STATE`)를 그대로 사용한다. | ||
|
|
||
| ## 관련 코드 | ||
|
|
||
| - `src/pages/SubscriptionsPage/SubscriptionsPage.tsx` — 페이지 (웹뷰/웹 분기) | ||
| - `src/pages/SubscriptionsPage/SubscriptionsPage.styles.ts` | ||
| - `src/hooks/useWebviewSubscribe.ts` — 구독 상태 브릿지 훅 (재사용) | ||
| - `src/hooks/Queries/useClub.ts` (`useGetCardList`) — 동아리 목록 (재사용) | ||
| - `src/pages/MainPage/components/ClubCard/ClubCard.tsx`, `src/pages/WebviewMainPage/components/SubscribeButton/SubscribeButton.tsx` — 카드/구독 버튼 (재사용) | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| - `src/utils/appStoreLink.ts` (`getAppStoreLink`) — 웹 CTA 링크 | ||
| - `src/routes/AppRoutes.tsx` — `/subscriptions` 라우트 | ||
|
|
||
| ## 참고 | ||
|
|
||
| 아직 공용 레이아웃(헤더/바텀네비)에 부착되지 않은 독립 페이지다. 부착은 레이아웃 셸(AppLayout) 단계에서 진행한다. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+514 Bytes
frontend/src/assets/images/icons/bottomNav/subscribe_unselected.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 51 additions & 0 deletions
51
frontend/src/components/common/BottomNavigation/BottomNavigation.stories.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import { MemoryRouter } from 'react-router-dom'; | ||
| import type { Meta, StoryObj } from '@storybook/react'; | ||
| import BottomNavigation from './BottomNavigation'; | ||
|
|
||
| const meta = { | ||
| title: 'Components/Common/BottomNavigation', | ||
| component: BottomNavigation, | ||
| parameters: { | ||
| layout: 'fullscreen', | ||
| docs: { | ||
| description: { | ||
| component: | ||
| '앱 네이티브 바텀탭을 웹으로 옮긴 하단 네비게이션입니다. (홈 / 구독 / 메뉴)', | ||
| }, | ||
| }, | ||
| }, | ||
| tags: ['autodocs'], | ||
| } satisfies Meta<typeof BottomNavigation>; | ||
|
|
||
| export default meta; | ||
| type Story = StoryObj<typeof meta>; | ||
|
|
||
| export const Home: Story = { | ||
| decorators: [ | ||
| (Story) => ( | ||
| <MemoryRouter initialEntries={['/']}> | ||
| <Story /> | ||
| </MemoryRouter> | ||
| ), | ||
| ], | ||
| }; | ||
|
|
||
| export const Subscriptions: Story = { | ||
| decorators: [ | ||
| (Story) => ( | ||
| <MemoryRouter initialEntries={['/subscriptions']}> | ||
| <Story /> | ||
| </MemoryRouter> | ||
| ), | ||
| ], | ||
| }; | ||
|
|
||
| export const Menu: Story = { | ||
| decorators: [ | ||
| (Story) => ( | ||
| <MemoryRouter initialEntries={['/menu']}> | ||
| <Story /> | ||
| </MemoryRouter> | ||
| ), | ||
| ], | ||
| }; |
44 changes: 44 additions & 0 deletions
44
frontend/src/components/common/BottomNavigation/BottomNavigation.styles.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import styled from 'styled-components'; | ||
| import { theme } from '@/styles/theme'; | ||
| import { Z_INDEX } from '@/styles/zIndex'; | ||
|
|
||
| export const Nav = styled.nav` | ||
| position: fixed; | ||
| bottom: 0; | ||
| left: 0; | ||
| right: 0; | ||
| display: flex; | ||
|
seongwon030 marked this conversation as resolved.
|
||
| background-color: #ffffff; | ||
| border-top: 1px solid #f0f0f0; | ||
| padding-top: 6px; | ||
| padding-bottom: calc(6px + env(safe-area-inset-bottom)); | ||
| z-index: ${Z_INDEX.bottomNav}; | ||
| `; | ||
|
|
||
| export const Tab = styled.button<{ $active: boolean }>` | ||
| flex: 1; | ||
| display: flex; | ||
| flex-direction: column; | ||
| align-items: center; | ||
| justify-content: center; | ||
| gap: 4px; | ||
| border: none; | ||
| background: none; | ||
| padding: 0; | ||
| cursor: pointer; | ||
| color: ${({ $active }) => | ||
| $active ? theme.colors.primary[900] : theme.colors.gray[500]}; | ||
| `; | ||
|
|
||
| export const ImageIcon = styled.img` | ||
| width: 28px; | ||
| height: 28px; | ||
| object-fit: contain; | ||
| `; | ||
|
|
||
| export const Label = styled.span` | ||
| font-size: 10px; | ||
| font-weight: 500; | ||
| line-height: 1; | ||
| color: inherit; | ||
| `; | ||
96 changes: 96 additions & 0 deletions
96
frontend/src/components/common/BottomNavigation/BottomNavigation.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import { useLocation, useNavigate } from 'react-router-dom'; | ||
| import HomeIcon from '@/assets/images/icons/bottomNav/home.svg?react'; | ||
| import MenuIcon from '@/assets/images/icons/bottomNav/menu.svg?react'; | ||
| import subscribeSelected from '@/assets/images/icons/bottomNav/subscribe_selected.png'; | ||
| import subscribeUnselected from '@/assets/images/icons/bottomNav/subscribe_unselected.png'; | ||
| import { USER_EVENT } from '@/constants/eventName'; | ||
| import useMixpanelTrack from '@/hooks/Mixpanel/useMixpanelTrack'; | ||
| import * as Styled from './BottomNavigation.styles'; | ||
|
|
||
| type SvgComponent = React.ComponentType<React.SVGProps<SVGSVGElement>>; | ||
|
|
||
| type TabIcon = | ||
| | { type: 'vector'; Component: SvgComponent } | ||
| | { type: 'image'; active: string; inactive: string }; | ||
|
|
||
| interface BottomNavTab { | ||
| key: string; | ||
| label: string; | ||
| path: string; | ||
| icon: TabIcon; | ||
| } | ||
|
|
||
| const TABS: BottomNavTab[] = [ | ||
| { | ||
| key: 'home', | ||
| label: '홈', | ||
| path: '/', | ||
| icon: { type: 'vector', Component: HomeIcon }, | ||
| }, | ||
| { | ||
| key: 'explore', | ||
| label: '구독', | ||
| path: '/subscriptions', | ||
| icon: { | ||
| type: 'image', | ||
| active: subscribeSelected, | ||
| inactive: subscribeUnselected, | ||
| }, | ||
| }, | ||
| { | ||
| key: 'more', | ||
| label: '메뉴', | ||
| path: '/menu', | ||
| icon: { type: 'vector', Component: MenuIcon }, | ||
| }, | ||
| ]; | ||
|
|
||
| const isTabActive = (pathname: string, path: string) => | ||
| path === '/' ? pathname === '/' : pathname.startsWith(path); | ||
|
seongwon030 marked this conversation as resolved.
Outdated
|
||
|
|
||
| const renderIcon = (icon: TabIcon, active: boolean) => { | ||
| if (icon.type === 'vector') { | ||
| const Icon = icon.Component; | ||
| return <Icon width={28} height={28} aria-hidden />; | ||
| } | ||
| return ( | ||
| <Styled.ImageIcon | ||
| src={active ? icon.active : icon.inactive} | ||
| alt='' | ||
| aria-hidden | ||
| /> | ||
| ); | ||
| }; | ||
|
|
||
| const BottomNavigation = () => { | ||
| const { pathname } = useLocation(); | ||
| const navigate = useNavigate(); | ||
| const trackEvent = useMixpanelTrack(); | ||
|
|
||
| const handleTabClick = (tab: BottomNavTab) => { | ||
| trackEvent(USER_EVENT.BOTTOM_TAB_CLICKED, { tab: tab.key, path: tab.path }); | ||
| navigate(tab.path); | ||
| }; | ||
|
|
||
| return ( | ||
| <Styled.Nav aria-label='하단 네비게이션'> | ||
| {TABS.map((tab) => { | ||
| const active = isTabActive(pathname, tab.path); | ||
| return ( | ||
| <Styled.Tab | ||
| key={tab.key} | ||
| type='button' | ||
| $active={active} | ||
| aria-current={active ? 'page' : undefined} | ||
| onClick={() => handleTabClick(tab)} | ||
| > | ||
| {renderIcon(tab.icon, active)} | ||
| <Styled.Label>{tab.label}</Styled.Label> | ||
| </Styled.Tab> | ||
| ); | ||
| })} | ||
| </Styled.Nav> | ||
| ); | ||
| }; | ||
|
|
||
| export default BottomNavigation; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.