diff --git a/.gitignore b/.gitignore index 9ff119e..95ef0be 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,7 @@ app-example /android # claude -.claudeignore CLAUDE.md -CONVENTION.md \ No newline at end of file +.claude/ +.claudeignore +CONVENTION.md diff --git a/src/app/(tabs)/_layout.tsx b/src/app/(tabs)/_layout.tsx index ef71d9f..622da57 100644 --- a/src/app/(tabs)/_layout.tsx +++ b/src/app/(tabs)/_layout.tsx @@ -1,15 +1,32 @@ +import type { BottomTabBarProps } from "@react-navigation/bottom-tabs"; import { Tabs } from "expo-router"; +import type { UserType } from "@/entities/user/model/types"; +import { BottomTabBar } from "@/widgets/bottom-tab-bar/ui/BottomTabBar"; + +// TODO: 추후 useAuthStore((s) => s.userType) 로 교체 +const TEMP_USER_TYPE: UserType = "company"; + +function TabBarAdapter({ state, navigation }: BottomTabBarProps) { + const activeRouteName = state.routes[state.index]?.name ?? "index"; + + return ( + navigation.navigate(routeName)} + /> + ); +} export default function TabLayout() { return ( - - null, - }} - /> + + + + + + + ); } diff --git a/src/app/(tabs)/account.tsx b/src/app/(tabs)/account.tsx new file mode 100644 index 0000000..16639e1 --- /dev/null +++ b/src/app/(tabs)/account.tsx @@ -0,0 +1,10 @@ +import { Text, View } from "react-native"; + +// TODO: @/pages/account-page/ui/AccountPage 로 교체 +export default function AccountScreen() { + return ( + + 계정관리 + + ); +} diff --git a/src/app/(tabs)/chat.tsx b/src/app/(tabs)/chat.tsx new file mode 100644 index 0000000..a5ce16f --- /dev/null +++ b/src/app/(tabs)/chat.tsx @@ -0,0 +1,10 @@ +import { Text, View } from "react-native"; + +// TODO: @/pages/chat-page/ui/ChatPage 로 교체 +export default function ChatScreen() { + return ( + + 채팅 + + ); +} diff --git a/src/app/(tabs)/coupons.tsx b/src/app/(tabs)/coupons.tsx new file mode 100644 index 0000000..313f895 --- /dev/null +++ b/src/app/(tabs)/coupons.tsx @@ -0,0 +1,10 @@ +import { Text, View } from "react-native"; + +// TODO: @/pages/coupons-page/ui/CouponsPage 로 교체 +export default function CouponsScreen() { + return ( + + 제휴권의함 + + ); +} diff --git a/src/app/(tabs)/dashboard.tsx b/src/app/(tabs)/dashboard.tsx new file mode 100644 index 0000000..11e638e --- /dev/null +++ b/src/app/(tabs)/dashboard.tsx @@ -0,0 +1,10 @@ +import { Text, View } from "react-native"; + +// TODO: @/pages/dashboard-page/ui/DashboardPage 로 교체 +export default function DashboardScreen() { + return ( + + 대시보드 + + ); +} diff --git a/src/app/(tabs)/nearby.tsx b/src/app/(tabs)/nearby.tsx new file mode 100644 index 0000000..e29a319 --- /dev/null +++ b/src/app/(tabs)/nearby.tsx @@ -0,0 +1,10 @@ +import { Text, View } from "react-native"; + +// TODO: @/pages/nearby-page/ui/NearbyPage 로 교체 +export default function NearbyScreen() { + return ( + + 내 주변 + + ); +} diff --git a/src/entities/user/model/types.ts b/src/entities/user/model/types.ts new file mode 100644 index 0000000..8364375 --- /dev/null +++ b/src/entities/user/model/types.ts @@ -0,0 +1 @@ +export type UserType = "customer" | "manager" | "company"; diff --git a/src/shared/styles/global.styles.css b/src/shared/styles/global.styles.css index 6577dc7..f470bc0 100644 --- a/src/shared/styles/global.styles.css +++ b/src/shared/styles/global.styles.css @@ -14,6 +14,7 @@ /* 회색 계열 */ --gray-100: #f4f4f5; --gray-300: #dbdde1; + --gray-400: #b4b4b4; --gray-500: #8e9398; /* 흰색 */ @@ -46,6 +47,7 @@ /* Text Colors - Primitive 참조 */ --color-content-primary: var(--black); --color-content-secondary: var(--gray-500); + --color-content-tertiary: var(--gray-400); --color-content-inverse: var(--blue-50); /* Layout Spacing */ @@ -69,6 +71,7 @@ /* 회색 계열 */ --gray-100: #f4f4f5; --gray-300: #dbdde1; + --gray-400: #b4b4b4; --gray-500: #8e9398; /* 흰색 */ @@ -101,6 +104,7 @@ /* Text Colors - Primitive 참조 */ --color-content-primary: var(--black); --color-content-secondary: var(--gray-500); + --color-content-tertiary: var(--gray-400); --color-content-inverse: var(--blue-50); /* Layout Spacing */ diff --git a/src/shared/styles/tokens.ts b/src/shared/styles/tokens.ts index 0af87b3..a4c5a3f 100644 --- a/src/shared/styles/tokens.ts +++ b/src/shared/styles/tokens.ts @@ -28,6 +28,10 @@ export const colorTokens = { contentPrimary: "#040404", /** global.styles.css: --color-content-secondary (gray-500) */ contentSecondary: "#8E9398", + /** contentSecondary at opacity 0.3 — RN은 hex + opacity 조합 불가하므로 별도 정의 */ + contentSecondaryAlpha30: "rgba(142, 147, 152, 0.3)", + /** global.styles.css: --color-content-tertiary (gray-400) */ + contentTertiary: "#B4B4B4", /** global.styles.css: --color-content-inverse (blue-50) */ contentInverse: "#F4F6FE", } as const; diff --git a/src/widgets/bottom-tab-bar/model/tabConfig.ts b/src/widgets/bottom-tab-bar/model/tabConfig.ts new file mode 100644 index 0000000..d5dad59 --- /dev/null +++ b/src/widgets/bottom-tab-bar/model/tabConfig.ts @@ -0,0 +1,109 @@ +import type { Ionicons } from "@expo/vector-icons"; +import type { ComponentProps } from "react"; +import type { UserType } from "@/entities/user/model/types"; + +type IoniconName = ComponentProps["name"]; + +/** + * TODO: SVG 머지 후 activeIconName/inactiveIconName 필드를 + * Icon: ComponentType<{ size: number; color: string }> 으로 교체 + */ +export interface TabItem { + route: string; + label: string; + activeIconName: IoniconName; + inactiveIconName: IoniconName; +} + +export const TAB_CONFIG: Record = { + customer: [ + { + route: "index", + label: "홈", + activeIconName: "home", + inactiveIconName: "home-outline", + }, + { + route: "nearby", + label: "내 주변", + activeIconName: "location", + inactiveIconName: "location-outline", + }, + { + route: "coupons", + label: "제휴권의함", + activeIconName: "pricetag", + inactiveIconName: "pricetag-outline", + }, + { + route: "account", + label: "계정관리", + activeIconName: "person", + inactiveIconName: "person-outline", + }, + ], + manager: [ + { + route: "index", + label: "홈", + activeIconName: "home", + inactiveIconName: "home-outline", + }, + { + route: "nearby", + label: "주변 매장", + activeIconName: "location", + inactiveIconName: "location-outline", + }, + { + route: "dashboard", + label: "대시보드", + activeIconName: "bar-chart", + inactiveIconName: "bar-chart-outline", + }, + { + route: "chat", + label: "채팅", + activeIconName: "chatbubble", + inactiveIconName: "chatbubble-outline", + }, + { + route: "account", + label: "계정관리", + activeIconName: "person", + inactiveIconName: "person-outline", + }, + ], + company: [ + { + route: "index", + label: "홈", + activeIconName: "home", + inactiveIconName: "home-outline", + }, + { + route: "nearby", + label: "주변 업체", + activeIconName: "location", + inactiveIconName: "location-outline", + }, + { + route: "dashboard", + label: "대시보드", + activeIconName: "bar-chart", + inactiveIconName: "bar-chart-outline", + }, + { + route: "chat", + label: "채팅", + activeIconName: "chatbubble", + inactiveIconName: "chatbubble-outline", + }, + { + route: "account", + label: "계정관리", + activeIconName: "person", + inactiveIconName: "person-outline", + }, + ], +}; diff --git a/src/widgets/bottom-tab-bar/ui/BottomTabBar.tsx b/src/widgets/bottom-tab-bar/ui/BottomTabBar.tsx new file mode 100644 index 0000000..803f4ff --- /dev/null +++ b/src/widgets/bottom-tab-bar/ui/BottomTabBar.tsx @@ -0,0 +1,49 @@ +import { StyleSheet, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import type { UserType } from "@/entities/user/model/types"; +import { shadows } from "@/shared/styles/shadows"; +import { colorTokens } from "@/shared/styles/tokens"; +import { TAB_CONFIG } from "../model/tabConfig"; +import { BottomTabItem } from "./BottomTabItem"; + +interface BottomTabBarProps { + userType: UserType; + activeRouteName: string; + onTabPress: (routeName: string) => void; +} + +export function BottomTabBar({ + userType, + activeRouteName, + onTabPress, +}: BottomTabBarProps) { + const insets = useSafeAreaInsets(); + const tabs = TAB_CONFIG[userType]; + + return ( + + {tabs.map((tab) => ( + onTabPress(tab.route)} + /> + ))} + + ); +} diff --git a/src/widgets/bottom-tab-bar/ui/BottomTabItem.tsx b/src/widgets/bottom-tab-bar/ui/BottomTabItem.tsx new file mode 100644 index 0000000..450f1c6 --- /dev/null +++ b/src/widgets/bottom-tab-bar/ui/BottomTabItem.tsx @@ -0,0 +1,38 @@ +import { Ionicons } from "@expo/vector-icons"; +import type { ComponentProps } from "react"; +import { Pressable, Text } from "react-native"; +import { colorTokens } from "@/shared/styles/tokens"; + +type IoniconName = ComponentProps["name"]; + +interface BottomTabItemProps { + label: string; + activeIconName: IoniconName; + inactiveIconName: IoniconName; + isActive: boolean; + onPress: () => void; +} + +export function BottomTabItem({ + label, + activeIconName, + inactiveIconName, + isActive, + onPress, +}: BottomTabItemProps) { + const color = isActive ? colorTokens.primary : colorTokens.contentTertiary; + const iconName = isActive ? activeIconName : inactiveIconName; + + return ( + + + + {label} + + + ); +} diff --git a/tailwind.config.js b/tailwind.config.js index 918baee..efdc334 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -22,6 +22,7 @@ module.exports = { // 글자 색상 "content-primary": "var(--color-content-primary)", "content-secondary": "var(--color-content-secondary)", + "content-tertiary": "var(--color-content-tertiary)", "content-inverse": "var(--color-content-inverse)", }, opacity: {