diff --git a/README.md b/README.md index a2c0ae8..a644451 100644 --- a/README.md +++ b/README.md @@ -137,13 +137,16 @@ src/app/index.tsx # 시작 src/app/terms.tsx # 약관 동의 src/app/profile.tsx # 프로필 작성 및 수정 src/app/settings.tsx # 설정 +src/app/(tabs)/ # 하단 탭 (홈, 일정, 학생, 리포트, 수입) src/features/onboarding/ # 화면 UI 및 데모 공유 상태 +src/features/main/ # 탭 화면 UI app.json # Expo 앱 설정 ``` ### 현재 구현 범위 -- 시작 → 약관 동의 → 프로필 → 설정 화면 +- 시작 → 약관 동의 → 프로필 → 하단 탭 화면, 홈에서 설정으로 이동 +- 하단 탭 화면은 이동만 가능한 빈 화면이며 내용은 아직 구현 전입니다. - 필수 약관 선택, 이름 필수 입력, 과목 복수 선택, 프로필 수정 - Expo Image로 번들에 포함된 SVG를 표시하며 웹 전용 절대 경로를 사용하지 않습니다. - Safe Area 및 iOS 키보드 회피 처리 diff --git a/src/app/(tabs)/_layout.tsx b/src/app/(tabs)/_layout.tsx new file mode 100644 index 0000000..a3015fa --- /dev/null +++ b/src/app/(tabs)/_layout.tsx @@ -0,0 +1,42 @@ +import { Link, Redirect, Tabs } from 'expo-router'; +import { StyleSheet, Text, View } from 'react-native'; +import { MainTabBar } from '@/features/main/MainTabBar'; +import { useOnboarding } from '@/features/onboarding/OnboardingProvider'; + +export default function TabsLayout() { + const { profile } = useOnboarding(); + if (!profile.name) return ; + return ( + + + } + screenOptions={{ headerTitleAlign: 'center' }} + > + ( + + 설정 + + ), + }} + /> + + + + + + + + ); +} + +const s = StyleSheet.create({ + page: { flex: 1, alignItems: 'center', backgroundColor: '#f3f4f7' }, + phone: { flex: 1, width: '100%', maxWidth: 390 }, + settings: { paddingHorizontal: 16, paddingVertical: 8 }, + settingsText: { color: '#343845', fontSize: 14 }, +}); diff --git a/src/app/(tabs)/home.tsx b/src/app/(tabs)/home.tsx new file mode 100644 index 0000000..43135d2 --- /dev/null +++ b/src/app/(tabs)/home.tsx @@ -0,0 +1,4 @@ +import PlaceholderScreen from '@/features/main/PlaceholderScreen'; +export default function Screen() { + return ; +} diff --git a/src/app/(tabs)/income.tsx b/src/app/(tabs)/income.tsx new file mode 100644 index 0000000..5a43d35 --- /dev/null +++ b/src/app/(tabs)/income.tsx @@ -0,0 +1,4 @@ +import PlaceholderScreen from '@/features/main/PlaceholderScreen'; +export default function Screen() { + return ; +} diff --git a/src/app/(tabs)/reports.tsx b/src/app/(tabs)/reports.tsx new file mode 100644 index 0000000..c251814 --- /dev/null +++ b/src/app/(tabs)/reports.tsx @@ -0,0 +1,4 @@ +import PlaceholderScreen from '@/features/main/PlaceholderScreen'; +export default function Screen() { + return ; +} diff --git a/src/app/(tabs)/schedule.tsx b/src/app/(tabs)/schedule.tsx new file mode 100644 index 0000000..192200b --- /dev/null +++ b/src/app/(tabs)/schedule.tsx @@ -0,0 +1,4 @@ +import PlaceholderScreen from '@/features/main/PlaceholderScreen'; +export default function Screen() { + return ; +} diff --git a/src/app/(tabs)/students.tsx b/src/app/(tabs)/students.tsx new file mode 100644 index 0000000..94db842 --- /dev/null +++ b/src/app/(tabs)/students.tsx @@ -0,0 +1,4 @@ +import PlaceholderScreen from '@/features/main/PlaceholderScreen'; +export default function Screen() { + return ; +} diff --git a/src/features/main/MainTabBar.tsx b/src/features/main/MainTabBar.tsx new file mode 100644 index 0000000..b72dc93 --- /dev/null +++ b/src/features/main/MainTabBar.tsx @@ -0,0 +1,68 @@ +import type { BottomTabBarProps } from 'expo-router/js-tabs'; +import { Pressable, StyleSheet, Text, View } from 'react-native'; +import { primitive, semantic } from '@/styles/colors'; +import { gap, padding } from '@/styles/measurement'; +import { fontSize, fontWeight, letterSpacing, lineHeight } from '@/styles/typography'; + +// 아이콘 프레임(여백 포함) 크기 +const ICON_SIZE = 32; + +export function MainTabBar({ + state, + descriptors, + emitter, + navigateToTab, + insets, +}: BottomTabBarProps) { + return ( + + {state.routes.map((route, index) => { + const focused = index === state.index; + const { options } = descriptors[route.key]; + const label = options.title ?? route.name; + const onPress = () => { + const event = emitter.emit({ + type: 'tabPress', + target: route.key, + canPreventDefault: true, + }); + if (!focused && !event.defaultPrevented) navigateToTab(route.key); + }; + return ( + + + {label} + + ); + })} + + ); +} + +const s = StyleSheet.create({ + bar: { + flexDirection: 'row', + justifyContent: 'space-between', + paddingTop: padding[16], + paddingHorizontal: padding[24], + backgroundColor: primitive.white[0], + }, + item: { alignItems: 'center', gap: gap[4] }, + icon: { width: ICON_SIZE, height: ICON_SIZE }, + label: { + fontSize: fontSize[12], + lineHeight: lineHeight[12], + fontWeight: fontWeight.semiBold, + letterSpacing: letterSpacing.none, + textAlign: 'center', + }, + labelActive: { color: semantic.action.default }, + labelInactive: { color: semantic.icon.secondary }, +}); diff --git a/src/features/main/PlaceholderScreen.tsx b/src/features/main/PlaceholderScreen.tsx new file mode 100644 index 0000000..53b3346 --- /dev/null +++ b/src/features/main/PlaceholderScreen.tsx @@ -0,0 +1,18 @@ +import { StyleSheet, Text, View } from 'react-native'; + +export default function PlaceholderScreen({ title }: { title: string }) { + return ( + + + {title} + + 화면 준비 중입니다. + + ); +} + +const s = StyleSheet.create({ + page: { flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: '#fff' }, + title: { color: '#343845', fontSize: 18, fontWeight: '700', marginBottom: 8 }, + body: { color: '#7c8391', fontSize: 14 }, +}); diff --git a/src/features/onboarding/OnboardingApp.tsx b/src/features/onboarding/OnboardingApp.tsx index 893efe0..7e668c3 100644 --- a/src/features/onboarding/OnboardingApp.tsx +++ b/src/features/onboarding/OnboardingApp.tsx @@ -333,8 +333,13 @@ export default function OnboardingApp({ screen }: { screen: Screen }) { {primary(editing ? '저장' : '완료', !validName, () => { setProfile({ name: name.trim(), subjects: selected }); - if (editing && router.canGoBack()) router.back(); - else router.replace('/settings'); + if (editing) { + if (router.canGoBack()) router.back(); + else router.replace('/settings'); + return; + } + if (router.canDismiss()) router.dismissAll(); + router.replace('/home'); })} )}