Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 87 additions & 13 deletions apps/native/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,106 @@
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<WebView>(null);
const [canGoBack, setCanGoBack] = useState(false);
const [exitApp, setExitApp] = useState(false);
const exitTimeout = useRef<NodeJS.Timeout | null>(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 (
<View style={styles.container}>
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<WebView
ref={webViewRef}
source={{ uri: WEB_URL }}
style={styles.webview}
javaScriptEnabled={true}
domStorageEnabled={true}
startInLoadingState={true}
sharedCookiesEnabled={true}
thirdPartyCookiesEnabled={true}
injectedJavaScript={INJECTED_JAVASCRIPT}
scalesPageToFit={true}
bounces={false}
scrollEnabled={true}
showsVerticalScrollIndicator={false}
allowsInlineMediaPlayback={true}
mediaPlaybackRequiresUserAction={false}
onNavigationStateChange={(navState) => {
setCanGoBack(navState.canGoBack);
}}
/>
<SimpleNavBar onNavigate={handleNavigate} />
</View>
</SafeAreaView>
);
}

Expand All @@ -38,6 +111,7 @@ const styles = StyleSheet.create({
},
webview: {
flex: 1,
backgroundColor: '#ffffff',
},
});

26 changes: 26 additions & 0 deletions apps/native/src/components/Logo.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Svg width={width} height={finalHeight} viewBox="0 0 26 24" fill="none">
<Path
d="M22.6201 9.84965L14.9951 2.33174C13.7845 1.15176 11.8618 1.16419 10.6658 2.36768L7.73397 5.1137C10.1881 4.13217 13.1415 4.82295 14.2058 7.86184C15.2044 10.3218 13.2751 13.0041 13.4256 15.0912C13.9055 17.6037 16.5249 18.3465 19.2274 17.3896L22.6478 14.1552C23.8278 12.9454 23.8147 11.0233 22.6104 9.82741L22.6201 9.84965Z"
fill="#FFA4A4"
/>
<Path
d="M10.3726 15.0246C10.5765 12.3222 12.6275 10.7755 12.2818 8.38988C11.9373 6.3734 9.50859 5.52186 7.61668 6.15474C6.41154 6.5436 4.78386 8.09214 4.78386 8.09214L3.32788 9.57967C2.14788 10.7895 2.16095 12.7115 3.36526 13.9074L10.9902 21.4254C12.2008 22.6053 14.1235 22.5929 15.3195 21.3894L16.6708 20.0263C13.6964 20.599 10.16 18.8663 10.4045 15.0372L10.3726 15.0246Z"
fill="#FF7878"
/>
</Svg>
);
};
53 changes: 53 additions & 0 deletions apps/native/src/components/LogoName.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Svg width={width} height={finalHeight} viewBox="0 0 61 14" fill="none">
<Path
d="M5.88776 7.85864H10.2745C10.2486 8.33767 10.1709 8.77785 10.0286 9.15331C9.84744 9.61939 9.58864 10.0207 9.26514 10.3573C8.94163 10.681 8.56637 10.927 8.07465 11.1471C7.03943 11.5096 5.88776 11.5614 4.80079 11.0694C4.24436 10.7975 3.79146 10.4739 3.4162 10.0596C3.02799 9.65823 2.71743 9.15331 2.47157 8.53187C2.23864 7.94927 2.12218 7.31488 2.12218 6.64165C2.12218 5.96842 2.23864 5.32109 2.45863 4.75143C2.65273 4.19473 2.96329 3.67686 3.3515 3.23667C3.72676 2.82238 4.16672 2.48576 4.69727 2.21388C5.22782 1.9679 5.79718 1.83843 6.39243 1.83843C7.23354 1.83843 7.997 2.04558 8.66989 2.45987C9.34278 2.913 9.88626 3.45677 10.2874 4.1041L10.4556 4.36303L12.0602 3.21078L11.9049 2.97774C11.2709 2.03263 10.5074 1.30762 9.54982 0.776801C8.59225 0.258934 7.55704 0 6.44419 0C5.52544 0 4.67139 0.168307 3.89498 0.491974C3.09269 0.828588 2.40687 1.29467 1.85044 1.87727C1.26813 2.47282 0.828169 3.15899 0.504665 3.98758C0.168222 4.79027 0 5.6836 0 6.64165C0 7.59971 0.181162 8.57071 0.530546 9.3734C0.866989 10.189 1.34577 10.9011 1.94102 11.4837C2.51039 12.0404 3.20915 12.4936 3.9985 12.8172C4.81373 13.1279 5.61602 13.2704 6.44419 13.2704C7.36294 13.2704 8.21699 13.115 8.98045 12.7913C9.73098 12.4547 10.3392 12.0275 10.8697 11.4708C11.3614 10.9141 11.7496 10.2279 12.0343 9.46403C12.2931 8.72607 12.4225 7.88453 12.4225 6.99121V5.98137H5.91364V7.83275L5.88776 7.85864Z"
fill="#2B2B2B"
/>
<Path
d="M21.0538 8.97205C21.028 9.3734 20.9374 9.73591 20.7821 10.0466C20.6009 10.3832 20.4068 10.6422 20.1869 10.8105C19.941 11.0047 19.6563 11.16 19.3716 11.2766C19.0352 11.3801 18.7505 11.4319 18.4788 11.4319C16.7577 11.4319 15.9554 10.5774 15.9554 8.73901V3.49561H13.8203V9.16625C13.8203 9.77475 13.9238 10.3185 14.1179 10.8234C14.325 11.3542 14.6226 11.7944 14.9979 12.1569C15.3861 12.5324 15.826 12.8043 16.3307 12.9985C16.8483 13.1927 17.4306 13.2962 18.0647 13.2962C18.6987 13.2962 19.3587 13.1797 20.0186 12.9337C20.4327 12.7525 20.808 12.5065 21.1444 12.1958L21.2221 12.9855H23.176V3.49561H21.0409V8.97205H21.0538Z"
fill="#2B2B2B"
/>
<Path
d="M28.52 10.8882C28.3647 10.7458 28.2871 10.5387 28.2871 10.2797V5.34705H30.5516V3.49567H28.2871V0.32373H26.152V3.49567H24.1592V5.34705H26.152V10.4739C26.152 10.8494 26.2167 11.1601 26.3461 11.4579C26.4625 11.7557 26.6437 12.0275 26.8766 12.2476C27.1095 12.4677 27.3425 12.636 27.6271 12.7784C27.9248 12.9079 28.2353 12.9726 28.5329 12.9726H30.7069V11.1213H29.2705C28.9341 11.1213 28.6753 11.0436 28.52 10.8882Z"
fill="#2B2B2B"
/>
<Path
d="M59.7188 6.21447C59.46 5.60598 59.1235 5.07516 58.6836 4.59614C58.2695 4.18184 57.7648 3.83228 57.1825 3.5604C56.5873 3.30147 55.992 3.18495 55.3709 3.18495C54.5039 3.18495 53.7016 3.34031 52.977 3.65103C52.5241 3.85818 52.1229 4.11711 51.7865 4.44078V0.32373H49.6514V12.9726H51.6053L51.7088 11.9499C52.0712 12.2994 52.5111 12.5972 53.0287 12.8173C53.7146 13.128 54.4392 13.2834 55.2415 13.2834C55.2803 13.2834 55.3191 13.2834 55.3709 13.2834C56.0309 13.2704 56.639 13.1409 57.2213 12.882C57.7648 12.6231 58.2824 12.2606 58.7224 11.8204C59.1494 11.3673 59.4729 10.8494 59.7447 10.2409C59.9776 9.61945 60.107 8.95917 60.107 8.26005C60.107 7.56093 59.9776 6.86181 59.7317 6.21447H59.7188ZM53.7404 11.1989C53.3652 11.0436 53.0676 10.8235 52.8217 10.5516C52.5629 10.2797 52.3688 9.95606 52.2394 9.56766C52.0971 9.17926 52.0194 8.75202 52.0194 8.26005C52.0194 7.76807 52.0971 7.31494 52.2394 6.90065C52.3688 6.51225 52.5629 6.17563 52.8088 5.89081C53.0546 5.61893 53.3781 5.41178 53.7275 5.25642C54.1287 5.10106 54.5427 5.02338 54.9956 5.02338C55.3968 5.02338 55.7979 5.10106 56.1861 5.24347C56.5355 5.39883 56.8461 5.61893 57.1049 5.86491C57.3637 6.16269 57.5837 6.51225 57.726 6.8877C57.8813 7.31494 57.9589 7.76807 57.9589 8.23416C57.9589 8.70024 57.8813 9.12748 57.726 9.54177C57.5707 9.93017 57.3766 10.2538 57.1049 10.5257C56.8461 10.7847 56.5355 11.0047 56.1861 11.1731C55.345 11.4967 54.478 11.4579 53.7275 11.1731L53.7404 11.1989Z"
fill="#2B2B2B"
/>
<Path d="M33.8382 0.32373H31.6772V12.9726H33.8382V0.32373Z" fill="#2B2B2B" />
<Path d="M48.2538 3.49561H46.0928V12.9726H48.2538V3.49561Z" fill="#2B2B2B" />
<Path
d="M33.8513 7.32901C34.0325 4.0276 38.0957 1.74899 40.5284 4.55842C41.913 6.00845 41.3048 8.42948 41.9907 9.85361C43.1165 11.6273 45.4327 11.2389 47.115 9.49111L47.3349 9.58173C47.2185 13.2715 41.1884 14.8122 39.8297 10.6693C39.2215 8.7143 40.2308 7.05713 39.325 5.47763C38.5227 4.15707 36.5299 4.18296 35.43 5.20575C34.8218 5.74951 34.356 6.53926 34.0713 7.36785L33.8384 7.32901H33.8513Z"
fill="url(#paint0_linear_303_1241)"
/>
<Defs>
<LinearGradient
id="paint0_linear_303_1241"
x1="44.4344"
y1="13.1435"
x2="38.0716"
y2="4.91888"
gradientUnits="userSpaceOnUse"
>
<Stop stopColor="#FF7878" />
<Stop offset="1" stopColor="#FFA4A4" />
</LinearGradient>
</Defs>
</Svg>
);
};
25 changes: 15 additions & 10 deletions apps/native/src/components/navBar/SimpleNavBar.tsx
Original file line number Diff line number Diff line change
@@ -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: '🏠' },
Expand All @@ -21,7 +20,15 @@ export function SimpleNavBar({ onNavigate }: SimpleNavBarProps) {
};

return (
<View style={styles.container}>
<View
style={[
styles.container,
{
paddingBottom: Math.max(insets.bottom, 10),
height: 70 + Math.max(insets.bottom, 10),
}
]}
>
{tabs.map((tab) => {
const isActive = activeTab === tab.name;

Expand Down Expand Up @@ -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',
Expand Down
21 changes: 21 additions & 0 deletions apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<html lang="ko">
<head>
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="format-detection" content="telephone=no" />
</head>
<body>
<QueryProvider>{children}</QueryProvider>
</body>
Expand Down
Binary file added apps/web/public/BottomNav/tab-home.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/BottomNav/tab-microorganismTest.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/BottomNav/tab-mypage.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/BottomNav/tab-record.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/BottomNav/tab-shopping.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
71 changes: 71 additions & 0 deletions apps/web/src/components/BottomNavBar.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<nav className="fixed bottom-0 left-0 right-0 h-[71px] bg-white border-t border-[#e0e0e0] shadow-[0_-2px_10px_rgba(0,0,0,0.1)] z-50">
{/* 홈 버튼 위 곡선 배경 */}
<div className="absolute left-1/2 -translate-x-1/2 -top-[45px] w-[62px] h-[72px] pointer-events-none">
<svg width="100%" height="100%" viewBox="0 0 100 50" className="drop-shadow-[0_-5px_7px_rgba(0,0,0,0.2)]">
<path d="M0,50 A50,57 0 0,1 100,50" fill="white" stroke="none" />
</svg>
</div>

<div className="flex items-center justify-between h-full px-4">
{tabs.map((tab) => {
const isActive = pathname === tab.href;
const isHome = tab.name === 'home';

return (
<Link
key={tab.name}
href={tab.href}
className={`flex-1 flex flex-col items-center justify-center gap-[3px] ${isHome ? 'relative' : ''}`}
>
{isHome ? (
<>
<div className="absolute -top-[40px] w-12 h-12 rounded-full bg-[#FF7878] flex items-center justify-center shadow-[0_4px_8px_rgba(0,0,0,0.25)] z-10">
<Image
src="/BottomNav/tab-home.png"
alt="홈"
width={26}
height={24}
className="object-contain"
/>
</div>
<span className="text-xs font-medium text-[#494949] mt-[14px]">{tab.label}</span>
</>
) : (
<>
<Image
src={`/BottomNav/tab-${tab.name}.png`}
alt={tab.label}
width={24}
height={24}
className={`object-contain ${isActive ? 'opacity-100' : 'opacity-60'}`}
/>
<span className={`text-xs ${isActive ? 'text-[#FF6B6B] font-semibold' : 'text-[#C2C2C2] font-normal'}`}>
{tab.label}
</span>
</>
)}
</Link>
);
})}
</div>
</nav>
);
}
24 changes: 24 additions & 0 deletions apps/web/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}

Expand Down
Loading