diff --git a/apps/native/app/index.tsx b/apps/native/app/index.tsx index e6eb137..3f1ba8b 100644 --- a/apps/native/app/index.tsx +++ b/apps/native/app/index.tsx @@ -1,21 +1,87 @@ -import { View, StyleSheet } from 'react-native'; +import { useRef, useEffect, useState } from 'react'; +import { StyleSheet, BackHandler, ToastAndroid, Platform } from 'react-native'; import { WebView } from 'react-native-webview'; -import { SimpleNavBar } from '../src/components/navBar/SimpleNavBar'; -import { useRef } from 'react'; +import { SafeAreaView } from 'react-native-safe-area-context'; + const WEB_URL = 'https://guthub.shop'; +// 모바일 최적화를 위한 JavaScript 코드 +const INJECTED_JAVASCRIPT = ` + (function() { + // 모바일 viewport 설정 + const meta = document.createElement('meta'); + meta.name = 'viewport'; + meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover'; + document.head.appendChild(meta); + + // 터치 액션 최적화 + document.body.style.webkitTouchCallout = 'none'; + document.body.style.webkitUserSelect = 'none'; + document.body.style.touchAction = 'manipulation'; + + // 전체 화면 활성화 + document.documentElement.style.height = '100%'; + document.body.style.height = '100%'; + document.body.style.overflow = 'auto'; + + // 확대/축소 방지 + document.addEventListener('gesturestart', function (e) { + e.preventDefault(); + }); + + true; + })(); +`; + export default function Home() { const webViewRef = useRef(null); + const [canGoBack, setCanGoBack] = useState(false); + const [exitApp, setExitApp] = useState(false); + const exitTimeout = useRef(null); + + useEffect(() => { + const backHandler = BackHandler.addEventListener('hardwareBackPress', () => { + if (canGoBack && webViewRef.current) { + // WebView에서 뒤로 갈 수 있으면 뒤로 가기 + webViewRef.current.goBack(); + return true; + } else { + // 뒤로 갈 수 없으면 앱 종료 로직 + if (exitApp) { + // 두 번째 뒤로가기: 앱 종료 + BackHandler.exitApp(); + return true; + } else { + // 첫 번째 뒤로가기: 토스트 메시지 + setExitApp(true); + if (Platform.OS === 'android') { + ToastAndroid.show('한 번 더 누르면 종료됩니다', ToastAndroid.SHORT); + } + + // 2초 후 exitApp 상태 리셋 + if (exitTimeout.current) { + clearTimeout(exitTimeout.current); + } + exitTimeout.current = setTimeout(() => { + setExitApp(false); + }, 2000); - const handleNavigate = (path: string) => { - const url = `${WEB_URL}${path}`; - console.log('[Home] Navigating to:', url); - webViewRef.current?.injectJavaScript(`window.location.href = '${url}'; true;`); - }; + return true; + } + } + }); + + return () => { + backHandler.remove(); + if (exitTimeout.current) { + clearTimeout(exitTimeout.current); + } + }; + }, [canGoBack, exitApp]); return ( - + { + setCanGoBack(navState.canGoBack); + }} /> - - + ); } @@ -38,6 +111,7 @@ const styles = StyleSheet.create({ }, webview: { flex: 1, + backgroundColor: '#ffffff', }, }); diff --git a/apps/native/src/components/Logo.tsx b/apps/native/src/components/Logo.tsx new file mode 100644 index 0000000..92ce1d9 --- /dev/null +++ b/apps/native/src/components/Logo.tsx @@ -0,0 +1,26 @@ +import Svg, { Path } from 'react-native-svg'; + +interface LogoProps { + width?: number; + height?: number; +} + +export const Logo = ({ width = 80, height = 74 }: LogoProps) => { + // 원본 비율 유지 (26:24) + const aspectRatio = 26 / 24; + const calculatedHeight = width / aspectRatio; + const finalHeight = height || calculatedHeight; + + return ( + + + + + ); +}; diff --git a/apps/native/src/components/LogoName.tsx b/apps/native/src/components/LogoName.tsx new file mode 100644 index 0000000..b3cd361 --- /dev/null +++ b/apps/native/src/components/LogoName.tsx @@ -0,0 +1,53 @@ +import Svg, { Path, Defs, LinearGradient, Stop } from 'react-native-svg'; + +interface LogoNameProps { + width?: number; + height?: number; +} + +export const LogoName = ({ width = 183, height = 42 }: LogoNameProps) => { + // 원본 비율 유지 (61:14) + const aspectRatio = 61 / 14; + const calculatedHeight = width / aspectRatio; + const finalHeight = height || calculatedHeight; + + return ( + + + + + + + + + + + + + + + + ); +}; diff --git a/apps/native/src/components/navBar/SimpleNavBar.tsx b/apps/native/src/components/navBar/SimpleNavBar.tsx index 9a45e3c..5e9aa7d 100644 --- a/apps/native/src/components/navBar/SimpleNavBar.tsx +++ b/apps/native/src/components/navBar/SimpleNavBar.tsx @@ -1,12 +1,11 @@ import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; -import { useState } from 'react'; +import { useRouter, usePathname } from 'expo-router'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; -interface SimpleNavBarProps { - onNavigate: (path: string) => void; -} - -export function SimpleNavBar({ onNavigate }: SimpleNavBarProps) { - const [activeTab, setActiveTab] = useState('home'); +export function SimpleNavBar() { + const router = useRouter(); + const pathname = usePathname(); + const insets = useSafeAreaInsets(); const tabs = [ { name: 'home', path: '/', label: '홈', icon: '🏠' }, @@ -21,7 +20,15 @@ export function SimpleNavBar({ onNavigate }: SimpleNavBarProps) { }; return ( - + {tabs.map((tab) => { const isActive = activeTab === tab.name; @@ -51,12 +58,10 @@ const styles = StyleSheet.create({ bottom: 0, left: 0, right: 0, - height: 70, backgroundColor: '#ffffff', flexDirection: 'row', alignItems: 'center', justifyContent: 'space-around', - paddingBottom: 10, borderTopWidth: 1, borderTopColor: '#e0e0e0', shadowColor: '#000', diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 5291496..f635791 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -2,9 +2,30 @@ import { QueryProvider } from '@repo/shared'; import '../styles/global.css'; +export const metadata = { + viewport: { + width: 'device-width', + initialScale: 1, + maximumScale: 1, + userScalable: false, + viewportFit: 'cover', + }, + themeColor: '#ffffff', + appleWebApp: { + capable: true, + statusBarStyle: 'default', + }, +}; + export default function RootLayout({ children }: { children: React.ReactNode }) { return ( + + + + + + {children} diff --git a/apps/web/public/BottomNav/tab-home.png b/apps/web/public/BottomNav/tab-home.png new file mode 100644 index 0000000..f49b055 Binary files /dev/null and b/apps/web/public/BottomNav/tab-home.png differ diff --git a/apps/web/public/BottomNav/tab-microorganismTest.png b/apps/web/public/BottomNav/tab-microorganismTest.png new file mode 100644 index 0000000..630279b Binary files /dev/null and b/apps/web/public/BottomNav/tab-microorganismTest.png differ diff --git a/apps/web/public/BottomNav/tab-mypage.png b/apps/web/public/BottomNav/tab-mypage.png new file mode 100644 index 0000000..af888bc Binary files /dev/null and b/apps/web/public/BottomNav/tab-mypage.png differ diff --git a/apps/web/public/BottomNav/tab-record.png b/apps/web/public/BottomNav/tab-record.png new file mode 100644 index 0000000..ff5b856 Binary files /dev/null and b/apps/web/public/BottomNav/tab-record.png differ diff --git a/apps/web/public/BottomNav/tab-shopping.png b/apps/web/public/BottomNav/tab-shopping.png new file mode 100644 index 0000000..7559023 Binary files /dev/null and b/apps/web/public/BottomNav/tab-shopping.png differ diff --git a/apps/web/src/components/BottomNavBar.tsx b/apps/web/src/components/BottomNavBar.tsx new file mode 100644 index 0000000..682db6d --- /dev/null +++ b/apps/web/src/components/BottomNavBar.tsx @@ -0,0 +1,71 @@ +'use client'; + +import Image from 'next/image'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +const tabs = [ + { name: 'microorganismTest', href: '/microorganismTest', label: '미생물 검사' }, + { name: 'record', href: '/record', label: '기록' }, + { name: 'home', href: '/', label: '홈' }, + { name: 'shopping', href: '/shopping', label: '쇼핑' }, + { name: 'mypage', href: '/myPage', label: '마이' }, +]; + +export function BottomNavBar() { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/apps/web/styles/global.css b/apps/web/styles/global.css index 37567d4..2404164 100644 --- a/apps/web/styles/global.css +++ b/apps/web/styles/global.css @@ -42,6 +42,30 @@ body, system-ui, Roboto, "Helvetica Neue", "Segoe UI", "Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif; + /* 모바일 최적화 */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-tap-highlight-color: transparent; + touch-action: manipulation; + overscroll-behavior: none; + } + + * { + -webkit-tap-highlight-color: transparent; + } + + /* 모바일에서 텍스트 선택 방지 (필요한 경우) */ + @media (hover: none) and (pointer: coarse) { + body { + -webkit-user-select: none; + user-select: none; + } + + /* 입력 필드와 콘텐츠 영역은 선택 가능하도록 */ + input, textarea, [contenteditable] { + -webkit-user-select: auto; + user-select: auto; + } } } diff --git a/packages/main-feature/src/pages/general/index/components/Layout.tsx b/packages/main-feature/src/pages/general/index/components/Layout.tsx index fa3d2b9..a249797 100644 --- a/packages/main-feature/src/pages/general/index/components/Layout.tsx +++ b/packages/main-feature/src/pages/general/index/components/Layout.tsx @@ -1,10 +1,22 @@ -import { AppBar, Layout as SharedLayout } from "@repo/shared"; +import { AppBar, Layout as SharedLayout } from "@repo/shared"; +import dynamic from 'next/dynamic'; + +// 클라이언트 컴포넌트로 동적 로드 +const BottomNavBar = dynamic( + () => import('../../../../../../apps/web/src/components/BottomNavBar').then(mod => ({ default: mod.BottomNavBar })), + { ssr: false } +); const Layout = ({ children }: { children: React.ReactNode }) => { return (
} rightContent={} bgColor="bg-[#FFF5F5]" /> - {children} + +
+ {children} +
+
+
) } diff --git a/packages/user/src/components/auth/LoginScreen.tsx b/packages/user/src/components/auth/LoginScreen.tsx index 8495ce0..3ce38e4 100644 --- a/packages/user/src/components/auth/LoginScreen.tsx +++ b/packages/user/src/components/auth/LoginScreen.tsx @@ -5,6 +5,8 @@ import { useRouter } from 'expo-router'; import { useState, useEffect } from 'react'; import { View, StyleSheet, Alert, Platform } from 'react-native'; +import { Logo } from '../../../../native/src/components/Logo'; +import { LogoName } from '../../../../native/src/components/LogoName'; import { SocialLoginButtons } from './SocialLoginButtons'; import { SocialLoginWebView } from './SocialLoginWebView'; @@ -87,11 +89,9 @@ export const LoginScreen = () => { {/* 로고 영역 */} - {/* TODO: 실제 앱 로고 이미지로 교체 */} - - - GutHub - + + + 오직 내 만을 위한 장 건강 케어 @@ -143,24 +143,16 @@ const styles = StyleSheet.create({ flex: 1, justifyContent: 'center', alignItems: 'center', + gap: 16, }, - logoPlaceholder: { - width: 80, - height: 80, - borderRadius: 40, - backgroundColor: colors.main, - justifyContent: 'center', - alignItems: 'center', - marginBottom: 20, - }, - logoText: { - fontSize: 24, - color: colors.white, + logoNameContainer: { + marginTop: 8, }, description: { fontSize: 14, color: colors['Black-600'], textAlign: 'center', + marginTop: 8, }, bottomSection: { gap: 16,