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) => (
-
- ))}
-
-
-
-
-
- 앱 버전
-
- {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;
-