From e8033ac0bbf704b3e3f07eaba9ee6474e04fef05 Mon Sep 17 00:00:00 2001 From: DHSeo <ehdgus1358@ajou.ac.kr> Date: Sun, 1 Dec 2024 17:19:31 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=A9=94=EC=9D=B8=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?UI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 12 +- README.md | 302 +++++++++++++++--------------- android/app/build.gradle | 72 +++++++ android/build.gradle | 37 ++++ android/settings.gradle | 18 ++ setup.sh | 35 +++- src/App.js | 89 ++++++--- src/android/AndroidManifest.xml | 91 ++++----- src/assets/default-profile.png | Bin 0 -> 4582 bytes src/components/DeepLinkHandler.js | 92 +++++---- src/components/LocationTracker.js | 204 ++++++++++---------- src/package.json | 84 +++++---- src/screens/ActivityScreen.js | 179 ++++++++++++++++++ src/screens/HomeScreen.js | 223 ++++++++++++++++++++++ src/screens/MyPageScreen.js | 239 +++++++++++++++++++++++ sync.sh | 2 +- 16 files changed, 1266 insertions(+), 413 deletions(-) create mode 100644 android/app/build.gradle create mode 100644 android/build.gradle create mode 100644 android/settings.gradle create mode 100644 src/assets/default-profile.png create mode 100644 src/screens/ActivityScreen.js create mode 100644 src/screens/HomeScreen.js create mode 100644 src/screens/MyPageScreen.js diff --git a/Dockerfile b/Dockerfile index 19513c5..285f2f4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ -FROM reactnativecommunity/react-native-android - -WORKDIR /app -ENV PATH /app/node_modules/.bin:$PATH - -# 기본 도구 설치 +FROM reactnativecommunity/react-native-android + +WORKDIR /app +ENV PATH /app/node_modules/.bin:$PATH + +# 기본 도구 설치 RUN apt-get update && apt-get install -y git \ No newline at end of file diff --git a/README.md b/README.md index a8c6a5f..a034552 100644 --- a/README.md +++ b/README.md @@ -1,152 +1,152 @@ -# Bin Buddy App 개발 환경 - -## 개요 -이 프로젝트는 Docker를 사용하여 React Native 안드로이드 앱을 개발하기 위한 환경을 제공합니다. - -## 사전 요구사항 -- Docker가 설치되어 있어야 합니다 -- Git이 설치되어 있어야 합니다 - -## 처음 시작하는 방법 - -1. 환경 설정 저장소 클론 - -```bash -git clone https://git.ajou.ac.kr/myunsyeya/bin_buddy_app_env.git -cd bin_buddy_app_env -``` - -2. 실행 권한 부여 - -```bash -chmod +x *.sh -``` - -3. 개발 환경 구축 - -```bash -./setup.sh -``` - -## 개발 시작하기 - -1. 소스코드 수정 후 동기화 -src/ 디렉토리에서 코드를 수정한 후, 다음 명령어로 동기화합니다: -```bash -./sync.sh -``` - -2. APK 빌드 -다음 명령어로 APK를 빌드합니다: -```bash -./dev.sh -``` -빌드된 APK는 상위 디렉토리에 자동으로 복사됩니다. - -3. (선택사항) 도커 개발 환경 진입 -도커 환경에서 직접 작업이 필요한 경우: -```bash -./dev.sh shell -``` - -## 프로젝트 구조 -```text -bin_buddy_app_env/ # 메인 개발 및 git 저장소 -├── Dockerfile # 도커 이미지 설정 -├── setup.sh # 초기 환경 구축 스크립트 -├── dev.sh # 개발 환경 실행 스크립트 -├── README.md # 문서 -└── src/ # 앱 소스코드 - ├── App.js # 메인 앱 컴포넌트 (딥링크 처리) - ├── components/ # 재사용 가능한 컴포넌트 - ├── screens/ # 화면 컴포넌트 - └── android/ # 안드로이드 설정 - └── AndroidManifest.xml # 딥링크 및 앱 설정 - -bin_buddy_app/ # 빌드용 임시 디렉토리 -``` - -- `src/`: 실제 개발이 이루어지는 소스코드 디렉토리 - - React Native 컴포넌트 및 화면 - - 안드로이드 네이티브 설정 파일 -- `bin_buddy_app/`: 빌드 전용 임시 디렉토리 - - setup.sh 실행 시 생성되는 실행 가능한 RN 프로젝트 - - src/ 디렉토리의 코드가 이곳으로 복사되어 빌드됨 - -## 주의사항 -- 모든 소스코드는 src 디렉토리에서 개발하고 관리됩니다 -- bin_buddy_app은 빌드 검증용 임시 디렉토리이므로 이곳에서 직접 수정하지 마세요 -- 새로운 설정이나 코드는 src 디렉토리 내 적절한 위치에 추가해 주세요 - -## 커밋 컨벤션 - -커밋 메시지는 다음 형식을 따릅니다: -``` -<type>: <description> - -[optional body] -``` - -커밋 타입: -- feat: 새로운 기능 추가 -- fix: 버그 수정 -- docs: 문서 수정 -- style: 코드 포맷팅, 세미콜론 누락 등 (코드 변경 없음) -- refactor: 코드 리팩토링 -- test: 테스트 코드 추가/수정 -- chore: 빌드 프로세스, 환경 설정 변경 등 - -예시: -``` -feat: 사용자 위치 추적 기능 추가 - -- 실시간 위치 업데이트 -- 위치 정보 권한 처리 -- 에러 핸들링 추가 -``` - -## 개발 워크플로우 - -1. 새로운 기능 개발 시작: -```bash -# feature 브랜치 생성 -git checkout -b feature/기능이름 - -# 개발 및 커밋 -git add . -git commit -m "feat: 새로운 기능 추가" -``` - -2. 기능 개발 완료 후: -```bash -# 원격 저장소에 push -git push origin feature/기능이름 -``` - -3. GitLab에서: -- Merge Request 생성 -- Source: feature/기능이름 -- Target: main -- "Squash commits" 옵션 체크 -- 변경사항 리뷰 후 머지 - -4. 머지 완료 후 로컬 정리: -```bash -# 원격 저장소 변경사항 가져오기 -git fetch - -# main 브랜치를 원격과 동일하게 맞추기 -git checkout main -git reset --hard origin/main - -# 작업이 완료된 feature 브랜치 삭제 -git branch -D feature/기능이름 -``` - -이렇게 하면: -- 기능별로 독립적인 개발 가능 -- 커밋 히스토리 깔끔하게 관리 -- 코드 리뷰 용이 -- 로컬 브랜치 관리 용이 - +# Bin Buddy App 개발 환경 + +## 개요 +이 프로젝트는 Docker를 사용하여 React Native 안드로이드 앱을 개발하기 위한 환경을 제공합니다. + +## 사전 요구사항 +- Docker가 설치되어 있어야 합니다 +- Git이 설치되어 있어야 합니다 + +## 처음 시작하는 방법 + +1. 환경 설정 저장소 클론 + +```bash +git clone https://git.ajou.ac.kr/myunsyeya/bin_buddy_app_env.git +cd bin_buddy_app_env +``` + +2. 실행 권한 부여 + +```bash +chmod +x *.sh +``` + +3. 개발 환경 구축 + +```bash +./setup.sh +``` + +## 개발 시작하기 + +1. 소스코드 수정 후 동기화 +src/ 디렉토리에서 코드를 수정한 후, 다음 명령어로 동기화합니다: +```bash +./sync.sh +``` + +2. APK 빌드 +다음 명령어로 APK를 빌드합니다: +```bash +./dev.sh +``` +빌드된 APK는 상위 디렉토리에 자동으로 복사됩니다. + +3. (선택사항) 도커 개발 환경 진입 +도커 환경에서 직접 작업이 필요한 경우: +```bash +./dev.sh shell +``` + +## 프로젝트 구조 +```text +bin_buddy_app_env/ # 메인 개발 및 git 저장소 +├── Dockerfile # 도커 이미지 설정 +├── setup.sh # 초기 환경 구축 스크립트 +├── dev.sh # 개발 환경 실행 스크립트 +├── README.md # 문서 +└── src/ # 앱 소스코드 + ├── App.js # 메인 앱 컴포넌트 (딥링크 처리) + ├── components/ # 재사용 가능한 컴포넌트 + ├── screens/ # 화면 컴포넌트 + └── android/ # 안드로이드 설정 + └── AndroidManifest.xml # 딥링크 및 앱 설정 + +bin_buddy_app/ # 빌드용 임시 디렉토리 +``` + +- `src/`: 실제 개발이 이루어지는 소스코드 디렉토리 + - React Native 컴포넌트 및 화면 + - 안드로이드 네이티브 설정 파일 +- `bin_buddy_app/`: 빌드 전용 임시 디렉토리 + - setup.sh 실행 시 생성되는 실행 가능한 RN 프로젝트 + - src/ 디렉토리의 코드가 이곳으로 복사되어 빌드됨 + +## 주의사항 +- 모든 소스코드는 src 디렉토리에서 개발하고 관리됩니다 +- bin_buddy_app은 빌드 검증용 임시 디렉토리이므로 이곳에서 직접 수정하지 마세요 +- 새로운 설정이나 코드는 src 디렉토리 내 적절한 위치에 추가해 주세요 + +## 커밋 컨벤션 + +커밋 메시지는 다음 형식을 따릅니다: +``` +<type>: <description> + +[optional body] +``` + +커밋 타입: +- feat: 새로운 기능 추가 +- fix: 버그 수정 +- docs: 문서 수정 +- style: 코드 포맷팅, 세미콜론 누락 등 (코드 변경 없음) +- refactor: 코드 리팩토링 +- test: 테스트 코드 추가/수정 +- chore: 빌드 프로세스, 환경 설정 변경 등 + +예시: +``` +feat: 사용자 위치 추적 기능 추가 + +- 실시간 위치 업데이트 +- 위치 정보 권한 처리 +- 에러 핸들링 추가 +``` + +## 개발 워크플로우 + +1. 새로운 기능 개발 시작: +```bash +# feature 브랜치 생성 +git checkout -b feature/기능이름 + +# 개발 및 커밋 +git add . +git commit -m "feat: 새로운 기능 추가" +``` + +2. 기능 개발 완료 후: +```bash +# 원격 저장소에 push +git push origin feature/기능이름 +``` + +3. GitLab에서: +- Merge Request 생성 +- Source: feature/기능이름 +- Target: main +- "Squash commits" 옵션 체크 +- 변경사항 리뷰 후 머지 + +4. 머지 완료 후 로컬 정리: +```bash +# 원격 저장소 변경사항 가져오기 +git fetch + +# main 브랜치를 원격과 동일하게 맞추기 +git checkout main +git reset --hard origin/main + +# 작업이 완료된 feature 브랜치 삭제 +git branch -D feature/기능이름 +``` + +이렇게 하면: +- 기능별로 독립적인 개발 가능 +- 커밋 히스토리 깔끔하게 관리 +- 코드 리뷰 용이 +- 로컬 브랜치 관리 용이 + 해당 절차에 따라 진행해주시면 감사하겠습니다. \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..7e812ba --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,72 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + ext { + buildToolsVersion = "34.0.0" + minSdkVersion = 21 + compileSdkVersion = 34 + targetSdkVersion = 34 + ndkVersion = "25.1.8937393" + kotlinVersion = "1.8.0" // Kotlin 버전 추가 + + // React Native 관련 설정 + reactNativeArchitectures = ["x86_64", "arm64-v8a"] + hermesEnabled = true + enableSeparateBuildPerCPUArchitecture = true + enableProguardInReleaseBuilds = true + FLIPPER_VERSION = '0.125.0' + } + repositories { + google() + mavenCentral() + } + dependencies { + classpath("com.android.tools.build:gradle:8.1.0") + classpath("com.facebook.react:react-native-gradle-plugin") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") // Kotlin 플러그인 추가 + } +} + +apply plugin: "com.android.application" +apply plugin: "com.facebook.react" + +allprojects { + repositories { + maven { + // Android JSC가 여기에 있습니다 + url("$rootDir/../node_modules/jsc-android/dist") + } + mavenCentral() + google() + maven { url 'https://www.jitpack.io' } + maven { + // React Native 프리빌드 의존성을 위해 필요 + url("$rootDir/../node_modules/react-native/android") + } + maven { + // React Native 의존성을 위해 필요 + url("$rootDir/../node_modules/jsc-android/dist") + } + } + + // Kotlin 버전 강제 적용 + configurations.all { + resolutionStrategy { + force "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" + force "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlinVersion" + force "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" + force "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" + } + } +} + +// 새로운 프로젝트를 생성할 때 적용할 설정 +def REACT_NATIVE_VERSION = new File(['node', '--print', "require('react-native/package.json').version"].execute(null, rootDir).text.trim()) + +allprojects { + configurations.all { + resolutionStrategy { + force "com.facebook.react:react-native:" + REACT_NATIVE_VERSION + } + } +} \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..bc51141 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,37 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + ext { + buildToolsVersion = "34.0.0" + minSdkVersion = 21 + compileSdkVersion = 34 + targetSdkVersion = 34 + ndkVersion = "25.1.8937393" + kotlinVersion = "1.8.0" + + // React Native 관련 설정 + reactNativeArchitectures = ["x86_64", "arm64-v8a"] + hermesEnabled = true + } + repositories { + google() + mavenCentral() + } + dependencies { + classpath("com.android.tools.build:gradle:8.1.0") + classpath("com.facebook.react:react-native-gradle-plugin") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") + } +} + +allprojects { + repositories { + maven { + // Android JSC가 여기에 있습니다 + url("$rootDir/../node_modules/jsc-android/dist") + } + mavenCentral() + google() + maven { url 'https://www.jitpack.io' } + } +} \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..ae999ea --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,18 @@ +rootProject.name = 'bin_buddy_app' + +apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) +include ':app' +includeBuild('../node_modules/@react-native/gradle-plugin') + +// React Native 의존성 추가 +include ':react-native-screens' +project(':react-native-screens').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-screens/android') + +include ':react-native-safe-area-context' +project(':react-native-safe-area-context').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-safe-area-context/android') + +include ':react-native-vector-icons' +project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android') + +include ':@react-native-community_geolocation' +project(':@react-native-community_geolocation').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/geolocation/android') \ No newline at end of file diff --git a/setup.sh b/setup.sh index af8580d..a69c256 100755 --- a/setup.sh +++ b/setup.sh @@ -8,15 +8,15 @@ cd .. npx react-native init bin_buddy_app # 타입스크립트 관련 파일 제거 -rm bin_buddy_app/App.tsx -rm bin_buddy_app/tsconfig.json -rm bin_buddy_app/__tests__/App.test.tsx +rm -f bin_buddy_app/App.tsx +rm -f bin_buddy_app/tsconfig.json +rm -f bin_buddy_app/__tests__/App.test.tsx # src의 모든 소스코드를 bin_buddy_app/src로 복사 mkdir -p bin_buddy_app/src cp -rv "$ENV_DIR/src/"* bin_buddy_app/src/ -# AndroidManifest.xml 복사 (이미 src 복사에 포함되어 있지만 명시적으로 다시 한번) +# AndroidManifest.xml 복사 cp -v "$ENV_DIR/src/android/AndroidManifest.xml" bin_buddy_app/android/app/src/main/AndroidManifest.xml # index.js 새로 생성 @@ -28,13 +28,30 @@ import {name as appName} from './app.json'; AppRegistry.registerComponent(appName, () => App); EOF -# 필요한 의존성 설치 +# 필요한 의존성 설치 (버전 명시) cd bin_buddy_app -npm install -npm install @react-native-community/geolocation -# package.json 의존성 추가 -npm install @react-native-community/geolocation +# node_modules 디렉토리 제거 (sudo 사용) +if [ -d "node_modules" ]; then + sudo rm -rf node_modules +fi + +# package-lock.json 제거 +if [ -f "package-lock.json" ]; then + sudo rm package-lock.json +fi + +# 각 패키지를 개별적으로 설치 +npm install --save @react-navigation/native@6.1.9 +npm install --save @react-navigation/bottom-tabs@6.5.11 +npm install --save @react-navigation/native-stack@6.9.17 +npm install --save react-native-screens@3.22.1 +npm install --save react-native-safe-area-context@4.6.3 +npm install --save react-native-vector-icons@10.0.3 +npm install --save @react-native-community/geolocation@3.1.0 + +# 권한 수정 +sudo chown -R $USER:$USER . # 다시 env 디렉토리로 이동 cd "$ENV_DIR" diff --git a/src/App.js b/src/App.js index 4fa6a2a..5285024 100644 --- a/src/App.js +++ b/src/App.js @@ -1,22 +1,67 @@ -import React from 'react'; -import { View, StyleSheet } from 'react-native'; -import DeepLinkHandler from './components/DeepLinkHandler'; -import LocationTracker from './components/LocationTracker'; - -const App = () => { - return ( - <View style={styles.container}> - <DeepLinkHandler /> - <LocationTracker /> - </View> - ); -}; - -const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - }, -}); - -export default App; +import React from 'react'; +import { NavigationContainer } from '@react-navigation/native'; +import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; +import Icon from 'react-native-vector-icons/MaterialIcons'; + +import HomeScreen from './screens/HomeScreen'; +import ActivityScreen from './screens/ActivityScreen'; +import MyPageScreen from './screens/MyPageScreen'; +import DeepLinkHandler from './components/DeepLinkHandler'; + +const Tab = createBottomTabNavigator(); + +const App = () => { + const linking = { + prefixes: ['binbuddy://'], + config: { + screens: { + Home: 'home', + Activity: 'activity', + MyPage: 'mypage', + FindTrashBin: 'find/trashbin', + FindFoodTruck: 'find/foodtruck', + }, + }, + }; + + return ( + <NavigationContainer linking={linking}> + <DeepLinkHandler /> + <Tab.Navigator + screenOptions={({ route }) => ({ + tabBarIcon: ({ focused, color, size }) => { + let iconName; + if (route.name === 'Home') { + iconName = 'home'; + } else if (route.name === 'Activity') { + iconName = 'local-activity'; + } else if (route.name === 'MyPage') { + iconName = 'person'; + } + return <Icon name={iconName} size={size} color={color} />; + }, + tabBarActiveTintColor: '#1E88E5', + tabBarInactiveTintColor: 'gray', + })} + > + <Tab.Screen + name="Home" + component={HomeScreen} + options={{ title: '홈' }} + /> + <Tab.Screen + name="Activity" + component={ActivityScreen} + options={{ title: '활동' }} + /> + <Tab.Screen + name="MyPage" + component={MyPageScreen} + options={{ title: '마이페이지' }} + /> + </Tab.Navigator> + </NavigationContainer> + ); +}; + +export default App; diff --git a/src/android/AndroidManifest.xml b/src/android/AndroidManifest.xml index 246999d..67c0414 100644 --- a/src/android/AndroidManifest.xml +++ b/src/android/AndroidManifest.xml @@ -1,42 +1,49 @@ -<manifest xmlns:android="http://schemas.android.com/apk/res/android"> - - <uses-permission android:name="android.permission.INTERNET" /> - <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> - <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> - - <application - android:name=".MainApplication" - android:label="@string/app_name" - android:icon="@mipmap/ic_launcher" - android:roundIcon="@mipmap/ic_launcher_round" - android:allowBackup="false" - android:theme="@style/AppTheme" - android:supportsRtl="true"> - - <activity - android:name=".MainActivity" - android:label="@string/app_name" - android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode" - android:launchMode="singleTask" - android:windowSoftInputMode="adjustResize" - android:exported="true"> - - <!-- 기본 런처 설정 --> - <intent-filter> - <action android:name="android.intent.action.MAIN" /> - <category android:name="android.intent.category.LAUNCHER" /> - </intent-filter> - - <!-- 딥링크 설정 --> - <intent-filter android:autoVerify="true"> - <action android:name="android.intent.action.VIEW" /> - <category android:name="android.intent.category.DEFAULT" /> - <category android:name="android.intent.category.BROWSABLE" /> - <data - android:scheme="binbuddy" - android:host="route" - android:pathPattern="/.*" /> - </intent-filter> - </activity> - </application> -</manifest> +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> + + <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> + <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> + <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> + <uses-permission android:name="android.permission.VIBRATE"/> + + <application + android:name=".MainApplication" + android:label="@string/app_name" + android:icon="@mipmap/ic_launcher" + android:roundIcon="@mipmap/ic_launcher_round" + android:allowBackup="false" + android:theme="@style/AppTheme" + android:supportsRtl="true" + android:usesCleartextTraffic="true"> + + <activity + android:name=".MainActivity" + android:label="@string/app_name" + android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode" + android:launchMode="singleTask" + android:windowSoftInputMode="adjustResize" + android:exported="true" + android:screenOrientation="portrait"> + + <!-- 기본 런처 설정 --> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + + <!-- 딥링크 설정 --> + <intent-filter android:autoVerify="true"> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="binbuddy" android:host="route" android:pathPattern="/.*" /> + <data android:scheme="binbuddy" android:host="home" /> + <data android:scheme="binbuddy" android:host="activity" /> + <data android:scheme="binbuddy" android:host="mypage" /> + <data android:scheme="binbuddy" android:host="find" /> + <data android:scheme="binbuddy" android:host="find" android:pathPrefix="/trashbin" /> + <data android:scheme="binbuddy" android:host="find" android:pathPrefix="/foodtruck" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/src/assets/default-profile.png b/src/assets/default-profile.png new file mode 100644 index 0000000000000000000000000000000000000000..cff2a3cd395006de392508ac8ca955cb0df26ad9 GIT binary patch literal 4582 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 literal 0 HcmV?d00001 diff --git a/src/components/DeepLinkHandler.js b/src/components/DeepLinkHandler.js index aabc049..494b53a 100644 --- a/src/components/DeepLinkHandler.js +++ b/src/components/DeepLinkHandler.js @@ -1,42 +1,52 @@ -import React, { useEffect } from 'react'; -import { Alert } from 'react-native'; -import { Linking } from 'react-native'; - -const DeepLinkHandler = () => { - useEffect(() => { - const handleDeepLink = (event) => { - const { url } = event; - - // URL 파싱 - try { - const regex = /^binbuddy:\/\/route\/(.+)$/; - const match = url.match(regex); - - if (match) { - const description = decodeURIComponent(match[1]); - Alert.alert('위치 정보 수신', `수신된 정보: ${description}`); - } - } catch (error) { - Alert.alert('오류', '잘못된 URL 형식입니다'); - } - }; - - // 앱이 실행 중일 때 딥링크 처리 - Linking.addEventListener('url', handleDeepLink); - - // 앱이 실행 중이지 않을 때 딥링크로 앱이 열린 경우 처리 - Linking.getInitialURL().then((url) => { - if (url) { - handleDeepLink({ url }); - } - }); - - return () => { - Linking.removeEventListener('url', handleDeepLink); - }; - }, []); - - return null; -}; - +import { useEffect } from 'react'; +import { Linking } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; + +const DeepLinkHandler = () => { + const navigation = useNavigation(); + + useEffect(() => { + const handleDeepLink = ({ url }) => { + if (!url) return; + + // URL 파싱 + const route = url.replace('binbuddy://', ''); + + switch (route) { + case 'home': + navigation.navigate('Home'); + break; + case 'activity': + navigation.navigate('Activity'); + break; + case 'mypage': + navigation.navigate('MyPage'); + break; + case 'find/trashbin': + navigation.navigate('FindTrashBin'); + break; + case 'find/foodtruck': + navigation.navigate('FindFoodTruck'); + break; + } + }; + + // 앱이 실행 중일 때 딥링크 처리 + Linking.addEventListener('url', handleDeepLink); + + // 앱이 실행되지 않은 상태에서 딥링크로 열린 경우 + Linking.getInitialURL().then(url => { + if (url) { + handleDeepLink({ url }); + } + }); + + return () => { + Linking.removeEventListener('url', handleDeepLink); + }; + }, [navigation]); + + return null; +}; + export default DeepLinkHandler; \ No newline at end of file diff --git a/src/components/LocationTracker.js b/src/components/LocationTracker.js index 3ed45a0..56e832d 100644 --- a/src/components/LocationTracker.js +++ b/src/components/LocationTracker.js @@ -1,103 +1,103 @@ -import React, { useState, useEffect } from 'react'; -import { View, Text, StyleSheet, PermissionsAndroid } from 'react-native'; -import Geolocation from '@react-native-community/geolocation'; - -const LocationTracker = () => { - const [location, setLocation] = useState(null); - const [watchId, setWatchId] = useState(null); - - const requestLocationPermission = async () => { - try { - const granted = await PermissionsAndroid.request( - PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION, - { - title: "위치 정보 권한", - message: "쓰레기통 위치 확인을 위해 위치 정보 권한이 필요합니다.", - buttonNeutral: "나중에 묻기", - buttonNegative: "거부", - buttonPositive: "허용" - } - ); - return granted === PermissionsAndroid.RESULTS.GRANTED; - } catch (err) { - console.warn(err); - return false; - } - }; - - useEffect(() => { - const startLocationTracking = async () => { - const hasPermission = await requestLocationPermission(); - - if (hasPermission) { - // 초기 위치 가져오기 - Geolocation.getCurrentPosition( - (position) => { - console.log('Initial position:', position); - setLocation({ - latitude: position.coords.latitude, - longitude: position.coords.longitude, - }); - }, - (error) => { - console.error('Error getting location:', error); - }, - { enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 } - ); - - // 위치 변경 감지 시작 - const id = Geolocation.watchPosition( - (position) => { - console.log('Updated position:', position); - setLocation({ - latitude: position.coords.latitude, - longitude: position.coords.longitude, - }); - }, - (error) => { - console.error('Error watching location:', error); - }, - { enableHighAccuracy: true, distanceFilter: 10 } - ); - setWatchId(id); - } - }; - - startLocationTracking(); - - // cleanup - return () => { - if (watchId !== null) { - Geolocation.clearWatch(watchId); - } - }; - }, []); - - return ( - <View style={styles.container}> - {location ? ( - <> - <Text style={styles.text}>lat: {location.latitude.toFixed(6)}</Text> - <Text style={styles.text}>lon: {location.longitude.toFixed(6)}</Text> - </> - ) : ( - <Text style={styles.text}>위치 정보를 가져오는 중...</Text> - )} - </View> - ); -}; - -const styles = StyleSheet.create({ - container: { - padding: 20, - backgroundColor: '#f5f5f5', - borderRadius: 10, - margin: 10, - }, - text: { - fontSize: 16, - marginVertical: 5, - }, -}); - +import React, { useState, useEffect } from 'react'; +import { View, Text, StyleSheet, PermissionsAndroid } from 'react-native'; +import Geolocation from '@react-native-community/geolocation'; + +const LocationTracker = () => { + const [location, setLocation] = useState(null); + const [watchId, setWatchId] = useState(null); + + const requestLocationPermission = async () => { + try { + const granted = await PermissionsAndroid.request( + PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION, + { + title: "위치 정보 권한", + message: "쓰레기통 위치 확인을 위해 위치 정보 권한이 필요합니다.", + buttonNeutral: "나중에 묻기", + buttonNegative: "거부", + buttonPositive: "허용" + } + ); + return granted === PermissionsAndroid.RESULTS.GRANTED; + } catch (err) { + console.warn(err); + return false; + } + }; + + useEffect(() => { + const startLocationTracking = async () => { + const hasPermission = await requestLocationPermission(); + + if (hasPermission) { + // 초기 위치 가져오기 + Geolocation.getCurrentPosition( + (position) => { + console.log('Initial position:', position); + setLocation({ + latitude: position.coords.latitude, + longitude: position.coords.longitude, + }); + }, + (error) => { + console.error('Error getting location:', error); + }, + { enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 } + ); + + // 위치 변경 감지 시작 + const id = Geolocation.watchPosition( + (position) => { + console.log('Updated position:', position); + setLocation({ + latitude: position.coords.latitude, + longitude: position.coords.longitude, + }); + }, + (error) => { + console.error('Error watching location:', error); + }, + { enableHighAccuracy: true, distanceFilter: 10 } + ); + setWatchId(id); + } + }; + + startLocationTracking(); + + // cleanup + return () => { + if (watchId !== null) { + Geolocation.clearWatch(watchId); + } + }; + }, []); + + return ( + <View style={styles.container}> + {location ? ( + <> + <Text style={styles.text}>lat: {location.latitude.toFixed(6)}</Text> + <Text style={styles.text}>lon: {location.longitude.toFixed(6)}</Text> + </> + ) : ( + <Text style={styles.text}>위치 정보를 가져오는 중...</Text> + )} + </View> + ); +}; + +const styles = StyleSheet.create({ + container: { + padding: 20, + backgroundColor: '#f5f5f5', + borderRadius: 10, + margin: 10, + }, + text: { + fontSize: 16, + marginVertical: 5, + }, +}); + export default LocationTracker; \ No newline at end of file diff --git a/src/package.json b/src/package.json index 57ad9cb..8765a3d 100644 --- a/src/package.json +++ b/src/package.json @@ -1,40 +1,46 @@ -{ - "name": "bin_buddy_app", - "version": "0.0.1", - "private": true, - "scripts": { - "android": "react-native run-android", - "ios": "react-native run-ios", - "lint": "eslint .", - "start": "react-native start", - "test": "jest" - }, - "dependencies": { - "react": "18.3.1", - "react-native": "0.76.3", - "@react-native-community/geolocation": "^3.1.0" - }, - "devDependencies": { - "@babel/core": "^7.25.2", - "@babel/preset-env": "^7.25.3", - "@babel/runtime": "^7.25.0", - "@react-native-community/cli": "15.0.1", - "@react-native-community/cli-platform-android": "15.0.1", - "@react-native-community/cli-platform-ios": "15.0.1", - "@react-native/babel-preset": "0.76.3", - "@react-native/eslint-config": "0.76.3", - "@react-native/metro-config": "0.76.3", - "@react-native/typescript-config": "0.76.3", - "@types/react": "^18.2.6", - "@types/react-test-renderer": "^18.0.0", - "babel-jest": "^29.6.3", - "eslint": "^8.19.0", - "jest": "^29.6.3", - "prettier": "2.8.8", - "react-test-renderer": "18.3.1", - "typescript": "5.0.4" - }, - "engines": { - "node": ">=18" - } +{ + "name": "bin_buddy_app", + "version": "0.0.1", + "private": true, + "scripts": { + "android": "react-native run-android", + "ios": "react-native run-ios", + "lint": "eslint .", + "start": "react-native start", + "test": "jest" + }, + "dependencies": { + "react": "18.3.1", + "react-native": "0.76.3", + "@react-native-community/geolocation": "^3.1.0", + "@react-navigation/native": "^6.1.9", + "@react-navigation/bottom-tabs": "^6.5.11", + "@react-navigation/native-stack": "^6.9.17", + "react-native-screens": "^3.29.0", + "react-native-safe-area-context": "^4.9.0", + "react-native-vector-icons": "^10.0.3" + }, + "devDependencies": { + "@babel/core": "^7.25.2", + "@babel/preset-env": "^7.25.3", + "@babel/runtime": "^7.25.0", + "@react-native-community/cli": "15.0.1", + "@react-native-community/cli-platform-android": "15.0.1", + "@react-native-community/cli-platform-ios": "15.0.1", + "@react-native/babel-preset": "0.76.3", + "@react-native/eslint-config": "0.76.3", + "@react-native/metro-config": "0.76.3", + "@react-native/typescript-config": "0.76.3", + "@types/react": "^18.2.6", + "@types/react-test-renderer": "^18.0.0", + "babel-jest": "^29.6.3", + "eslint": "^8.19.0", + "jest": "^29.6.3", + "prettier": "2.8.8", + "react-test-renderer": "18.3.1", + "typescript": "5.0.4" + }, + "engines": { + "node": ">=18" + } } \ No newline at end of file diff --git a/src/screens/ActivityScreen.js b/src/screens/ActivityScreen.js new file mode 100644 index 0000000..b1b5257 --- /dev/null +++ b/src/screens/ActivityScreen.js @@ -0,0 +1,179 @@ +import React from 'react'; +import { View, TouchableOpacity, Text, StyleSheet, ScrollView } from 'react-native'; +import Icon from 'react-native-vector-icons/MaterialIcons'; +import { useNavigation } from '@react-navigation/native'; + +const ActivityScreen = () => { + const navigation = useNavigation(); + + const activities = [ + { + id: 'walk', + icon: 'directions-walk', + title: '걸기', + action: '시작하기', + color: '#1976D2', + }, + { + id: 'bike', + icon: 'directions-bike', + title: '자전거', + action: '출발하기', + color: '#388E3C', + }, + ]; + + const features = [ + { + id: 'trashbin', + icon: 'delete', + title: '가까운 쓰레기통 찾기', + color: '#1E88E5', + onPress: () => navigation.navigate('FindTrashBin'), + }, + { + id: 'foodtruck', + icon: 'local-dining', + title: '푸드트럭 찾기', + color: '#E64A19', + onPress: () => navigation.navigate('FindFoodTruck'), + }, + { + id: 'tumbler', + icon: 'local-cafe', + title: '텀블러 할인 카페 찾기', + color: '#7B1FA2', + onPress: () => {}, + }, + ]; + + return ( + <ScrollView style={styles.container}> + <View style={styles.header}> + <Text style={styles.headerTitle}>전체</Text> + <Text style={styles.headerSubtitle}>내 활동</Text> + </View> + + <View style={styles.activityGrid}> + {activities.map((activity) => ( + <TouchableOpacity + key={activity.id} + style={styles.activityCard} + > + <View style={[styles.iconContainer, { backgroundColor: `${activity.color}15` }]}> + <Icon name={activity.icon} size={32} color={activity.color} /> + </View> + <Text style={styles.activityTitle}>{activity.title}</Text> + <TouchableOpacity style={[styles.actionButton, { backgroundColor: activity.color }]}> + <Text style={styles.actionText}>{activity.action}</Text> + </TouchableOpacity> + </TouchableOpacity> + ))} + </View> + + <View style={styles.featureSection}> + {features.map((feature) => ( + <TouchableOpacity + key={feature.id} + style={styles.featureCard} + onPress={feature.onPress} + > + <View style={[styles.featureIconContainer, { backgroundColor: `${feature.color}15` }]}> + <Icon name={feature.icon} size={24} color={feature.color} /> + </View> + <Text style={styles.featureTitle}>{feature.title}</Text> + <Icon name="chevron-right" size={24} color="#666" /> + </TouchableOpacity> + ))} + </View> + </ScrollView> + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f8f9fa', + }, + header: { + padding: 20, + backgroundColor: '#fff', + borderBottomWidth: 1, + borderBottomColor: '#eee', + }, + headerTitle: { + fontSize: 24, + fontWeight: 'bold', + color: '#1a237e', + marginBottom: 4, + }, + headerSubtitle: { + fontSize: 14, + color: '#666', + }, + activityGrid: { + flexDirection: 'row', + padding: 12, + }, + activityCard: { + flex: 1, + margin: 4, + padding: 16, + backgroundColor: '#fff', + borderRadius: 12, + elevation: 2, + alignItems: 'center', + }, + iconContainer: { + width: 60, + height: 60, + borderRadius: 30, + justifyContent: 'center', + alignItems: 'center', + marginBottom: 12, + }, + activityTitle: { + fontSize: 16, + fontWeight: 'bold', + color: '#333', + marginBottom: 12, + }, + actionButton: { + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 20, + }, + actionText: { + color: '#fff', + fontSize: 14, + fontWeight: 'bold', + }, + featureSection: { + padding: 12, + }, + featureCard: { + flexDirection: 'row', + alignItems: 'center', + padding: 16, + backgroundColor: '#fff', + borderRadius: 12, + marginBottom: 8, + elevation: 2, + }, + featureIconContainer: { + width: 48, + height: 48, + borderRadius: 24, + justifyContent: 'center', + alignItems: 'center', + marginRight: 16, + }, + featureTitle: { + flex: 1, + fontSize: 16, + color: '#333', + fontWeight: '500', + }, +}); + +export default ActivityScreen; \ No newline at end of file diff --git a/src/screens/HomeScreen.js b/src/screens/HomeScreen.js new file mode 100644 index 0000000..d68084a --- /dev/null +++ b/src/screens/HomeScreen.js @@ -0,0 +1,223 @@ +import React from 'react'; +import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native'; +import Icon from 'react-native-vector-icons/MaterialIcons'; +import LocationTracker from '../components/LocationTracker'; + +const HomeScreen = () => { + return ( + <ScrollView style={styles.container}> + <View style={styles.header}> + <Text style={styles.title}>기후행동기회득</Text> + <TouchableOpacity style={styles.bellIcon}> + <Icon name="notifications-none" size={24} color="#333" /> + </TouchableOpacity> + </View> + + <View style={styles.pointSection}> + <View style={styles.pointHeader}> + <Text style={styles.pointLabel}>내 적립금</Text> + <View style={styles.badge}> + <Text style={styles.badgeText}>2024 시범사업 종료</Text> + </View> + </View> + <Text style={styles.pointAmount}>0원</Text> + <View style={styles.pointStamp}> + <Icon name="verified" size={20} color="#1E88E5" /> + <Text style={styles.stampText}>시범사업 종료</Text> + </View> + </View> + + <View style={styles.carbonSection}> + <View style={styles.carbonHeader}> + <Icon name="eco" size={24} color="#2E7D32" /> + <Text style={styles.sectionTitle}>온실가스 저감효과</Text> + </View> + <Text style={styles.carbonAmount}>0.0kgCO₂eq</Text> + </View> + + <View style={styles.quickActions}> + <TouchableOpacity style={styles.actionButton}> + <Icon name="directions-walk" size={24} color="#1E88E5" /> + <Text style={styles.actionText}>걸음수</Text> + <Text style={styles.actionValue}>시작하기</Text> + </TouchableOpacity> + + <TouchableOpacity style={styles.actionButton}> + <Icon name="directions-bike" size={24} color="#1E88E5" /> + <Text style={styles.actionText}>자전거</Text> + <Text style={styles.actionValue}>출발하기</Text> + </TouchableOpacity> + </View> + + <View style={styles.additionalFeatures}> + <TouchableOpacity style={styles.featureButton}> + <Icon name="local-cafe" size={24} color="#1E88E5" /> + <Text style={styles.featureText}>텀블러 할인 카페 찾기</Text> + </TouchableOpacity> + </View> + + <View style={styles.cardSection}> + <TouchableOpacity style={styles.card}> + <View style={styles.cardContent}> + <Text style={styles.cardTitle}>경기지역화폐 카드 연결하기</Text> + <Icon name="arrow-forward" size={24} color="#666" /> + </View> + </TouchableOpacity> + </View> + + <LocationTracker /> + </ScrollView> + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f8f9fa', + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: 16, + backgroundColor: '#fff', + elevation: 2, + }, + title: { + fontSize: 24, + fontWeight: 'bold', + color: '#1a237e', + }, + bellIcon: { + padding: 8, + }, + pointSection: { + margin: 16, + padding: 20, + backgroundColor: '#fff', + borderRadius: 12, + elevation: 3, + }, + pointHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + pointLabel: { + fontSize: 16, + color: '#666', + }, + pointAmount: { + fontSize: 32, + fontWeight: 'bold', + marginTop: 12, + color: '#1a237e', + }, + badge: { + backgroundColor: '#ff5252', + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 12, + }, + badgeText: { + color: '#fff', + fontSize: 12, + fontWeight: 'bold', + }, + pointStamp: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 12, + }, + stampText: { + marginLeft: 8, + color: '#1E88E5', + fontSize: 14, + }, + carbonSection: { + margin: 16, + marginTop: 0, + padding: 20, + backgroundColor: '#e8f5e9', + borderRadius: 12, + elevation: 3, + }, + carbonHeader: { + flexDirection: 'row', + alignItems: 'center', + }, + sectionTitle: { + fontSize: 16, + color: '#2e7d32', + marginLeft: 8, + fontWeight: 'bold', + }, + carbonAmount: { + fontSize: 28, + fontWeight: 'bold', + color: '#2e7d32', + marginTop: 12, + }, + quickActions: { + flexDirection: 'row', + justifyContent: 'space-between', + padding: 16, + }, + actionButton: { + flex: 1, + margin: 8, + padding: 16, + backgroundColor: '#fff', + borderRadius: 12, + alignItems: 'center', + elevation: 2, + }, + actionText: { + marginTop: 8, + fontSize: 14, + color: '#333', + }, + actionValue: { + marginTop: 4, + fontSize: 16, + color: '#1E88E5', + fontWeight: 'bold', + }, + additionalFeatures: { + padding: 16, + }, + featureButton: { + flexDirection: 'row', + alignItems: 'center', + padding: 16, + backgroundColor: '#fff', + borderRadius: 12, + elevation: 2, + }, + featureText: { + marginLeft: 12, + fontSize: 16, + color: '#333', + }, + cardSection: { + padding: 16, + }, + card: { + backgroundColor: '#fff', + borderRadius: 12, + padding: 20, + elevation: 2, + }, + cardContent: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + cardTitle: { + fontSize: 16, + color: '#333', + fontWeight: 'bold', + }, +}); + +export default HomeScreen; \ No newline at end of file diff --git a/src/screens/MyPageScreen.js b/src/screens/MyPageScreen.js new file mode 100644 index 0000000..2b65e00 --- /dev/null +++ b/src/screens/MyPageScreen.js @@ -0,0 +1,239 @@ +import React from 'react'; +import { View, Text, StyleSheet, TouchableOpacity, Image, ScrollView } from 'react-native'; +import Icon from 'react-native-vector-icons/MaterialIcons'; + +const MenuItem = ({ icon, title, showBorder = true }) => ( + <TouchableOpacity style={[styles.menuItem, showBorder && styles.menuBorder]}> + <View style={styles.menuContent}> + <Icon name={icon} size={22} color="#555" /> + <Text style={styles.menuText}>{title}</Text> + </View> + <Icon name="chevron-right" size={22} color="#999" /> + </TouchableOpacity> +); + +const MyPageScreen = () => { + const menuItems = [ + { icon: 'person', title: '개인정보관리' }, + { icon: 'description', title: '가입확인서 발급' }, + { icon: 'people', title: '추천인 등록' }, + { icon: 'campaign', title: '공지사항' }, + { icon: 'notifications', title: '알림마당' }, + { icon: 'help', title: '자주 묻는 질문' }, + { icon: 'headset-mic', title: '고객센터', showBorder: false } + ]; + + return ( + <ScrollView style={styles.container}> + <View style={styles.profileSection}> + <View style={styles.profileHeader}> + <View style={styles.profileInfo}> + <View style={styles.profileImageContainer}> + <Image + source={require('../assets/default-profile.png')} + style={styles.profileImage} + /> + <TouchableOpacity style={styles.editButton}> + <Icon name="edit" size={16} color="#fff" /> + </TouchableOpacity> + </View> + <View style={styles.nameSection}> + <Text style={styles.username}>jack1358</Text> + <TouchableOpacity style={styles.editProfile}> + <Text style={styles.editProfileText}>프로필 관리</Text> + <Icon name="chevron-right" size={16} color="#666" /> + </TouchableOpacity> + </View> + </View> + </View> + + <View style={styles.statsContainer}> + <View style={styles.statItem}> + <Text style={styles.statLabel}>내 적립 리워드</Text> + <Text style={styles.statValue}>0원</Text> + </View> + <View style={styles.statDivider} /> + <View style={styles.statItem}> + <Text style={styles.statLabel}>온실가스 저감효과</Text> + <Text style={[styles.statValue, styles.greenText]}>0kgCO₂eq</Text> + </View> + </View> + </View> + + <View style={styles.cardSection}> + <View style={styles.cardContainer}> + <TouchableOpacity style={styles.card}> + <Icon name="credit-card" size={24} color="#1E88E5" /> + <Text style={styles.cardText}>교통카드 등록</Text> + </TouchableOpacity> + <TouchableOpacity style={styles.card}> + <Icon name="account-balance-wallet" size={24} color="#1E88E5" /> + <Text style={styles.cardText}>경기지역화폐 카드 연결</Text> + </TouchableOpacity> + </View> + </View> + + <View style={styles.menuSection}> + {menuItems.map((item, index) => ( + <MenuItem + key={index} + icon={item.icon} + title={item.title} + showBorder={item.showBorder !== false} + /> + ))} + </View> + + <View style={styles.footer}> + <Text style={styles.version}>앱 버전 1.0.0</Text> + </View> + </ScrollView> + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f8f9fa', + }, + profileSection: { + backgroundColor: '#fff', + paddingTop: 20, + borderBottomWidth: 1, + borderBottomColor: '#eee', + }, + profileHeader: { + paddingHorizontal: 20, + }, + profileInfo: { + flexDirection: 'row', + alignItems: 'center', + }, + profileImageContainer: { + position: 'relative', + }, + profileImage: { + width: 80, + height: 80, + borderRadius: 40, + backgroundColor: '#f0f0f0', + }, + editButton: { + position: 'absolute', + right: 0, + bottom: 0, + backgroundColor: '#1E88E5', + width: 28, + height: 28, + borderRadius: 14, + justifyContent: 'center', + alignItems: 'center', + borderWidth: 2, + borderColor: '#fff', + }, + nameSection: { + marginLeft: 20, + flex: 1, + }, + username: { + fontSize: 24, + fontWeight: 'bold', + color: '#333', + }, + editProfile: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 8, + }, + editProfileText: { + color: '#666', + fontSize: 14, + }, + statsContainer: { + flexDirection: 'row', + paddingVertical: 20, + marginTop: 20, + backgroundColor: '#f8f9fa', + }, + statItem: { + flex: 1, + alignItems: 'center', + }, + statDivider: { + width: 1, + backgroundColor: '#ddd', + marginVertical: 10, + }, + statLabel: { + fontSize: 14, + color: '#666', + marginBottom: 8, + }, + statValue: { + fontSize: 20, + fontWeight: 'bold', + color: '#333', + }, + greenText: { + color: '#2E7D32', + }, + cardSection: { + padding: 20, + }, + cardContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + card: { + flex: 1, + backgroundColor: '#fff', + padding: 16, + borderRadius: 12, + alignItems: 'center', + marginHorizontal: 6, + elevation: 2, + }, + cardText: { + marginTop: 8, + fontSize: 12, + color: '#333', + textAlign: 'center', + }, + menuSection: { + backgroundColor: '#fff', + borderRadius: 12, + marginHorizontal: 20, + marginBottom: 20, + elevation: 2, + }, + menuItem: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: 16, + paddingHorizontal: 20, + }, + menuContent: { + flexDirection: 'row', + alignItems: 'center', + }, + menuBorder: { + borderBottomWidth: 1, + borderBottomColor: '#f0f0f0', + }, + menuText: { + fontSize: 16, + color: '#333', + marginLeft: 12, + }, + footer: { + padding: 20, + alignItems: 'center', + }, + version: { + color: '#999', + fontSize: 14, + }, +}); + +export default MyPageScreen; \ No newline at end of file diff --git a/sync.sh b/sync.sh index a2403ac..448b5e8 100755 --- a/sync.sh +++ b/sync.sh @@ -30,7 +30,7 @@ if [ -f "src/package.json" ]; then rm ../bin_buddy_app/package.json.bak cd ../bin_buddy_app - npm install + npm install --legacy-peer-deps cd ../bin_buddy_app_env fi -- GitLab