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');
})}
)}