diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx deleted file mode 100644 index 4f3b026..0000000 --- a/app/(tabs)/_layout.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import MenuIcon from "@/assets/icons/ic-menu.svg"; -import HomeIcon from "@/assets/icons/ic_home.svg"; -import { HapticTab } from "@/components/haptic-tab"; -import { USER_EVENT } from "@/constants/eventname"; -import { Colors } from "@/constants/theme"; -import { useMixpanelTrack } from "@/hooks"; -import { useColorScheme } from "@/hooks/use-color-scheme"; -import { Tabs } from "expo-router"; -import React from "react"; -import { Image } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; - -export default function TabLayout() { - const colorScheme = useColorScheme(); - const trackEvent = useMixpanelTrack(); - const insets = useSafeAreaInsets(); - const TAB_BAR_BASE_HEIGHT = 56; - const TAB_BAR_BASE_PADDING_VERTICAL = 6; - - const handleTabPress = (tabName: string) => { - trackEvent(USER_EVENT.BOTTOM_TAB_CLICKED, { - tab: tabName, - url: `app://moadong/(tabs)/${tabName}`, - }); - }; - - return ( - - ( - - ), - }} - listeners={{ - tabPress: () => handleTabPress("home"), - }} - /> - ( - - ), - }} - listeners={{ - tabPress: () => handleTabPress("explore"), - }} - /> - ( - - ), - }} - listeners={{ - tabPress: () => handleTabPress("more"), - }} - /> - - ); -} diff --git a/app/(tabs)/explore.tsx b/app/(tabs)/explore.tsx deleted file mode 100644 index 9417ca4..0000000 --- a/app/(tabs)/explore.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import SubscribeScreen from '@/ui/subscribe'; - -export default SubscribeScreen; diff --git a/app/(tabs)/index.tsx.backup b/app/(tabs)/index.tsx.backup deleted file mode 100644 index d4269c3..0000000 --- a/app/(tabs)/index.tsx.backup +++ /dev/null @@ -1,3 +0,0 @@ -import HomeScreen from '@/ui/home'; - -export default HomeScreen; diff --git a/app/(tabs)/more.tsx b/app/(tabs)/more.tsx deleted file mode 100644 index 42954c4..0000000 --- a/app/(tabs)/more.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import MoadongIcon from '@/assets/icons/ic-moadong.svg'; -import { MoaText } from '@/components/moa-text'; -import { PAGE_VIEW_EVENT, USER_EVENT } from '@/constants/eventname'; -import { useMixpanelTrack, useTrackScreenView } from '@/hooks'; -import { Ionicons } from '@expo/vector-icons'; -import Constants from 'expo-constants'; -import { useRouter } from 'expo-router'; -import React from 'react'; -import { TouchableOpacity, View } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import styled from 'styled-components/native'; - -interface MenuItem { - id: string; - title: string; - icon: keyof typeof Ionicons.glyphMap; - route: string; -} - -const menuItems: MenuItem[] = [ - { - id: 'introduce', - title: '서비스 소개', - icon: 'information-circle-outline', - route: '/webview/introduce', - }, - { - id: 'club-union', - title: '총 동아리 연합회', - icon: 'people-outline', - route: '/webview/club-union', - }, - { - id: 'privacy-policy', - title: '개인정보 처리방침', - icon: 'document-text-outline', - route: '/webview/privacy-policy', - }, -]; - -export default function MoreScreen() { - const insets = useSafeAreaInsets(); - const router = useRouter(); - const trackEvent = useMixpanelTrack(); - - useTrackScreenView(PAGE_VIEW_EVENT.MORE_PAGE); - - const appVersion = - Constants.expoConfig?.version ?? - Constants.nativeAppVersion ?? - Constants.manifest?.version ?? - '알 수 없음'; - - const handleMenuPress = (item: MenuItem) => { - trackEvent(USER_EVENT.MORE_MENU_CLICKED, { - menu: item.title, - url: `app://moadong${item.route}`, - }); - - router.push(item.route as any); - }; - - return ( - -
- 더보기 -
- - - {menuItems.map((item) => ( - handleMenuPress(item)} - activeOpacity={0.7} - > - - - - - {item.title} - - - - ))} - - - - - - 앱 버전 - - {appVersion} - - -
- ); -} - -// Styled Components -const Container = styled(View)` - flex: 1; - background-color: #fff; -`; - -const Header = styled.View` - padding-horizontal: 16px; - padding-vertical: 16px; - border-bottom-width: 1px; - border-bottom-color: #F0F0F0; -`; - -const HeaderTitle = styled(MoaText)` - color: #111111; -`; - -const MenuList = styled.View` - padding-top: 8px; - flex: 1; -`; - -const MenuItem = styled(TouchableOpacity)` - flex-direction: row; - align-items: center; - justify-content: space-between; - padding-horizontal: 16px; - padding-vertical: 16px; - border-bottom-width: 1px; - border-bottom-color: #F5F5F5; -`; - -const MenuItemContent = styled.View` - flex-direction: row; - align-items: center; - flex: 1; -`; - -const IconContainer = styled.View` - width: 40px; - height: 40px; - border-radius: 20px; - background-color: #FFECE5; - justify-content: center; - align-items: center; - margin-right: 12px; -`; - -const MenuItemText = styled(MoaText)` - color: #111111; - font-size: 16px; -`; - -const VersionItem = styled.View` - flex-direction: row; - align-items: center; - justify-content: space-between; - padding-horizontal: 16px; - padding-vertical: 16px; - border-bottom-width: 1px; - border-bottom-color: #ffffff; -`; - -const VersionValue = styled(MoaText)` - color: #888888; -`; - diff --git a/app/_layout.tsx b/app/_layout.tsx index 978d29f..21836cf 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -25,7 +25,7 @@ SplashScreen.preventAutoHideAsync().catch(() => { }); export const unstable_settings = { - anchor: '(tabs)', + anchor: 'index', }; type BootstrapStatus = 'idle' | 'running' | 'success' | 'failed'; @@ -144,7 +144,7 @@ export default function RootLayout() { - + diff --git a/app/(tabs)/index.tsx b/app/index.tsx similarity index 90% rename from app/(tabs)/index.tsx rename to app/index.tsx index a32e779..a0e6847 100644 --- a/app/(tabs)/index.tsx +++ b/app/index.tsx @@ -2,7 +2,7 @@ import { HomeScreen } from '@/ui/home/home-screen'; import { HomeWebViewScreen } from '@/ui/home/home-webview-screen'; import React, { useState } from 'react'; -export default function HomeTab() { +export default function Home() { const [webViewFailed, setWebViewFailed] = useState(false); if (webViewFailed) { diff --git a/app/webview/[slug].tsx b/app/webview/[slug].tsx index cd406ae..b683662 100644 --- a/app/webview/[slug].tsx +++ b/app/webview/[slug].tsx @@ -83,7 +83,7 @@ export default function WebViewScreen() { if (router.canGoBack()) { router.back(); } else { - router.push("/(tabs)/more"); + router.push("/"); } }; diff --git a/ui/club-detail/club-detail-screen.tsx b/ui/club-detail/club-detail-screen.tsx index df92b89..a1b45cb 100644 --- a/ui/club-detail/club-detail-screen.tsx +++ b/ui/club-detail/club-detail-screen.tsx @@ -71,7 +71,7 @@ export default function ClubWebViewScreen() { if (router.canGoBack()) { router.back(); } else { - router.push("/(tabs)"); + router.push("/"); } }; diff --git a/ui/home/home-webview-screen.tsx b/ui/home/home-webview-screen.tsx index c6da484..5b51dd3 100644 --- a/ui/home/home-webview-screen.tsx +++ b/ui/home/home-webview-screen.tsx @@ -1,6 +1,7 @@ import { useMixpanelContext } from '@/contexts'; import { useSubscribedClubsContext } from '@/contexts/subscribed-clubs-context'; import { appendSessionId, getWebViewUserAgent } from '@/utils/webview'; +import Constants from 'expo-constants'; import { useRouter } from 'expo-router'; import * as WebBrowser from 'expo-web-browser'; import React, { useCallback, useEffect, useRef, useState } from 'react'; @@ -81,6 +82,13 @@ export function HomeWebViewScreen({ onError }: HomeWebViewScreenProps) { case 'OPEN_EXTERNAL_URL': await WebBrowser.openBrowserAsync(payload.url); break; + + case 'REQUEST_APP_VERSION': + sendMessage({ + type: 'APP_VERSION', + payload: { version: Constants.expoConfig?.version ?? 'unknown' }, + }); + break; } } catch { // 파싱 실패 무시 diff --git a/ui/subscribe/components/empty-state.tsx b/ui/subscribe/components/empty-state.tsx deleted file mode 100644 index 74d73ca..0000000 --- a/ui/subscribe/components/empty-state.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/** - * 구독 빈 상태 컴포넌트 - */ - -import { MoaImage } from '@/components/moa-image'; -import { MoaText } from '@/components/moa-text'; -import { USER_EVENT } from '@/constants/eventname'; -import { MainColors } from '@/constants/theme'; -import { useMixpanelTrack } from '@/hooks'; -import { useRouter } from 'expo-router'; -import React from 'react'; -import { TouchableOpacity } from 'react-native'; -import styled from 'styled-components/native'; - -/** - * 구독한 동아리가 없을 때 표시되는 컴포넌트 - */ -export function EmptyState() { - const router = useRouter(); - const trackEvent = useMixpanelTrack(); - - const handleGoHome = () => { - trackEvent(USER_EVENT.GO_HOME_BUTTON_CLICKED, { - from: 'subscribe_empty', - url: 'app://moadong/(tabs)/home', - }); - - router.push('/(tabs)'); - }; - - return ( - - - - - - 구독한 동아리가 없어요 - - - 관심있는 동아리를 구독하고{'\n'}새로운 모집 및 활동 소식을 받아보세요 - - - - 홈으로 가기 - - - ); -} - -// Styled Components -const Container = styled.View` - flex: 1; - justify-content: center; - align-items: center; - padding: 40px; - background-color: #fff; -`; - -const IconContainer = styled.View` - margin-bottom: 24px; -`; - -const Title = styled(MoaText)` - color: #111111; - margin-bottom: 12px; - text-align: center; -`; - -const Description = styled(MoaText)` - color: #989898; - text-align: center; - margin-bottom: 32px; - line-height: 24px; -`; - -const HomeButton = styled(TouchableOpacity)` - background-color: ${MainColors.main}; - padding-horizontal: 32px; - padding-vertical: 14px; - border-radius: 12px; -`; - -const ButtonText = styled(MoaText)` - color: #FFFFFF; -`; - diff --git a/ui/subscribe/components/index.ts b/ui/subscribe/components/index.ts deleted file mode 100644 index 0984669..0000000 --- a/ui/subscribe/components/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * 구독 화면 컴포넌트 Export - */ - -export * from './empty-state'; -export * from './subscribed-club-list'; - diff --git a/ui/subscribe/components/subscribed-club-list.tsx b/ui/subscribe/components/subscribed-club-list.tsx deleted file mode 100644 index a2eb835..0000000 --- a/ui/subscribe/components/subscribed-club-list.tsx +++ /dev/null @@ -1,97 +0,0 @@ -/** - * 구독한 동아리 목록 컴포넌트 - */ - -import { Club } from '@/types/club.types'; -import { ClubCard } from '@/ui/home/components/club-card'; -import React, { RefObject } from 'react'; -import { ActivityIndicator, FlatList, RefreshControl } from 'react-native'; -import styled from 'styled-components/native'; - -/** - * 구독한 동아리 목록 Props - */ -interface SubscribedClubListProps { - clubs: Club[]; - loading: boolean; - onRefresh: () => void; - onClubPress: (club: Club) => void; - isSubscribed: (clubId: string) => boolean; - onSubscribeToggle: (club: Club) => Promise; - listRef?: RefObject>; -} - -/** - * 구독한 동아리 목록을 표시하는 컴포넌트 - */ -export function SubscribedClubList({ - clubs, - loading, - onRefresh, - onClubPress, - isSubscribed, - onSubscribeToggle, - listRef, -}: SubscribedClubListProps) { - const renderItem = ({ item }: { item: Club }) => ( - onSubscribeToggle(item)} - /> - ); - - const keyExtractor = (item: Club) => item.id; - - const renderListHeader = () => ( - - 총 {clubs.length}개의 동아리를 구독 중입니다 - - ); - - return ( - - } - showsVerticalScrollIndicator={false} - ListFooterComponent={ - loading ? ( - - - - ) : null - } - /> - ); -} - -// Styled Components -const HeaderContainer = styled.View` - padding-horizontal: 16px; - padding-vertical: 16px; -`; - -const CountText = styled.Text` - font-size: 14px; - color: #666666; - font-weight: 500; -`; - -const LoadingContainer = styled.View` - padding: 20px; - align-items: center; -`; - diff --git a/ui/subscribe/hook/index.ts b/ui/subscribe/hook/index.ts deleted file mode 100644 index 2e2fcb7..0000000 --- a/ui/subscribe/hook/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * 구독 화면 훅 Export - */ - -export * from './use-subscribe-screen'; - diff --git a/ui/subscribe/hook/use-subscribe-screen.ts b/ui/subscribe/hook/use-subscribe-screen.ts deleted file mode 100644 index 9c9cd45..0000000 --- a/ui/subscribe/hook/use-subscribe-screen.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * 구독 화면 로직 커스텀 훅 - */ - -import { Club } from '@/types/club.types'; -import { useClubs } from '@/ui/home/hook/use-clubs'; -import { useSubscribedClubs } from '@/ui/home/hook/use-subscribed-clubs'; -import { useCallback, useMemo } from 'react'; - -/** - * 구독 화면 훅 반환 값 - */ -interface UseSubscribeScreenReturn { - subscribedClubs: Club[]; - loading: boolean; - error: string | null; - refetch: () => void; - isSubscribed: (clubId: string) => boolean; - toggleSubscribe: (clubId: string) => Promise<{ needsPermission: boolean }>; -} - -/** - * 구독 화면 데이터를 관리하는 훅 - * - * @example - * ```typescript - * const { - * subscribedClubs, - * loading, - * error, - * refetch, - * isSubscribed, - * toggleSubscribe, - * } = useSubscribeScreen(); - * ``` - */ -export function useSubscribeScreen(): UseSubscribeScreenReturn { - // 구독 동아리 ID 목록 가져오기 - const { - subscribedClubIds, - isSubscribed, - toggleSubscribe, - } = useSubscribedClubs(); - - // 모든 동아리 데이터 가져오기 - const { - clubs, - loading, - error, - refetch, - } = useClubs({ - initialCategory: undefined, // 전체 카테고리 - initialType: 'central', // 중앙동아리 - autoFetch: true, - }); - - /** - * 구독한 동아리만 필터링 - */ - const subscribedClubs = useMemo(() => { - return clubs.filter(club => subscribedClubIds.includes(club.id)); - }, [clubs, subscribedClubIds]); - - /** - * 새로고침 핸들러 - */ - const handleRefetch = useCallback(() => { - refetch(); - }, [refetch]); - - return { - subscribedClubs, - loading, - error, - refetch: handleRefetch, - isSubscribed, - toggleSubscribe, - }; -} - diff --git a/ui/subscribe/index.ts b/ui/subscribe/index.ts deleted file mode 100644 index 6a55f08..0000000 --- a/ui/subscribe/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * 구독 화면 Export - */ - -export * from './subscribe-screen'; -export { default } from './subscribe-screen'; - diff --git a/ui/subscribe/subscribe-screen.tsx b/ui/subscribe/subscribe-screen.tsx deleted file mode 100644 index 7cb9e07..0000000 --- a/ui/subscribe/subscribe-screen.tsx +++ /dev/null @@ -1,206 +0,0 @@ -/** - * 구독 화면 컴포넌트 - */ - -import { MoaText } from '@/components/moa-text'; -import { PermissionDialog } from '@/components/permission-dialog'; -import { PAGE_VIEW_EVENT, USER_EVENT } from '@/constants/eventname'; -import { useMixpanelTrack, useTrackScreenView } from '@/hooks'; -import { Club } from '@/types/club.types'; -import { useRouter } from 'expo-router'; -import React, { RefObject, useCallback, useRef, useState } from 'react'; -import { ActivityIndicator, FlatList, TouchableOpacity, View } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import styled from 'styled-components/native'; -import { EmptyState, SubscribedClubList } from './components'; -import { useSubscribeScreen } from './hook'; - -/** - * 구독 화면 메인 컴포넌트 - */ -export function SubscribeScreen() { - const insets = useSafeAreaInsets(); - const router = useRouter(); - const listRef = useRef | null>(null); - const [showPermissionDialog, setShowPermissionDialog] = useState(false); - - useTrackScreenView(PAGE_VIEW_EVENT.SUBSCRIBE_PAGE); - const trackEvent = useMixpanelTrack(); - - // 구독 화면 데이터 및 로직 - const { - subscribedClubs, - loading, - error, - refetch, - isSubscribed, - toggleSubscribe, - } = useSubscribeScreen(); - - /** - * 동아리 카드 클릭 핸들러 - */ - const handleClubPress = useCallback((club: Club) => { - if (!club?.id) { - return; - } - - trackEvent(USER_EVENT.CLUB_CARD_CLICKED, { - clubName: club.name, - category: club.category, - from: 'subscribe', - url: 'app://moadong/(tabs)/subscribe', - }); - - router.push({ - pathname: '/club/[id]', - params: { id: club.id, name: club.name } - }); - }, [router, trackEvent]); - - /** - * 구독 토글 핸들러 - */ - const handleSubscribeToggle = useCallback(async (club: Club) => { - const wasSubscribed = isSubscribed(club.id); - - trackEvent(USER_EVENT.SUBSCRIBE_BUTTON_CLICKED, { - clubName: club.name, - subscribed: !wasSubscribed, - from: 'subscribe', - url: 'app://moadong/(tabs)/subscribe', - }); - - const result = await toggleSubscribe(club.id); - if (result.needsPermission) { - setShowPermissionDialog(true); - } - }, [toggleSubscribe, isSubscribed, trackEvent]); - - /** - * 로딩 중 표시 - */ - if (loading && subscribedClubs.length === 0) { - return ( - -
- 구독 -
- - - -
- ); - } - - /** - * 에러 발생 시 표시 - */ - if (error && subscribedClubs.length === 0) { - return ( - -
- 구독 -
- - 구독한 동아리 목록을 불러오지 못했어요. - - 재시도 - - -
- ); - } - - /** - * 구독한 동아리가 없을 때 - */ - if (subscribedClubs.length === 0) { - return ( - -
- 구독 -
- -
- ); - } - - /** - * 구독한 동아리 목록 표시 - */ - return ( - -
- 구독 -
- >} - /> - - {/* 알림 권한 다이얼로그 */} - setShowPermissionDialog(false)} - /> -
- ); -} - -// Styled Components -const Container = styled(View)` - flex: 1; - background-color: #fff; -`; - -const Header = styled.View` - padding-horizontal: 16px; - padding-vertical: 16px; - border-bottom-width: 1px; - border-bottom-color: #F0F0F0; -`; - -const HeaderTitle = styled(MoaText)` - color: #111111; -`; - -const LoadingContainer = styled.View` - flex: 1; - justify-content: center; - align-items: center; -`; - -const ErrorContainer = styled.View` - flex: 1; - justify-content: center; - align-items: center; - padding: 24px; -`; - -const ErrorTitle = styled(MoaText)` - color: #3A3A3A; - text-align: center; - margin-bottom: 16px; - font-size: 18px; -`; - -const RetryButton = styled(TouchableOpacity)` - background-color: #FF5414; - padding-horizontal: 28px; - padding-vertical: 14px; - border-radius: 8px; -`; - -const RetryButtonText = styled(MoaText)` - color: #fff; - font-size: 16px; -`; - -export default SubscribeScreen; -