diff --git a/ios/DDang/Info.plist b/ios/DDang/Info.plist
index 7b1a018..167e5f1 100644
--- a/ios/DDang/Info.plist
+++ b/ios/DDang/Info.plist
@@ -24,10 +24,14 @@
$(CURRENT_PROJECT_VERSION)
LSRequiresIPhoneOS
+ NMFClientId
+ j6vmx4vlbe
NSAppTransportSecurity
NSAllowsArbitraryLoads
+ NSAllowsLocalNetworking
+
NSExceptionDomains
ddang.site
@@ -38,13 +42,17 @@
- NSAllowsLocalNetworking
-
+ NSAppleMusicUsageDescription
+ 사진 등록시 사진 선택을 위해 라이브러리 접근 권한이 필요합니다.
+ NSCameraUsageDescription
+ 카메라 촬영시 카메라 접근 권한이 필요합니다.
NSLocationAlwaysAndWhenInUseUsageDescription
내 위치 정보 표시와 산책 기능 제공을 위해 권한 허용이 필요합니다. 설정에서 언제든 변경이 가능합니다.
NSLocationAlwaysUsageDescription
내 위치 정보 표시와 산책 기능 제공을 위해 권한 허용이 필요합니다. 설정에서 언제든 변경이 가능합니다.
+ NSLocationTemporaryUsageDescriptionDictionary
+ 네이버 맵에서 내 위치를 표시하기 위해 권한을 요청합니다.
NSLocationWhenInUseUsageDescription
내 위치 정보 표시와 산책 기능 제공을 위해 권한 허용이 필요합니다. 설정에서 언제든 변경이 가능합니다.
NSMotionUsageDescription
@@ -53,27 +61,8 @@
사진 등록시 사진 선택을 위해 라이브러리 접근 권한이 필요합니다.
NSPhotoLibraryUsageDescription
사진 등록시 사진 선택을 위해 라이브러리 접근 권한이 필요합니다.
- NSAppleMusicUsageDescription
- 사진 등록시 사진 선택을 위해 라이브러리 접근 권한이 필요합니다.
- NSCameraUsageDescription
- 카메라 촬영시 카메라 접근 권한이 필요합니다.
NSUserNotificationUsageDescription
앱에서 알림을 보내기 위해 권한이 필요합니다.
-
- UILaunchStoryboardName
- LaunchScreen
- UIRequiredDeviceCapabilities
-
- arm64
-
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UIViewControllerBasedStatusBarAppearance
-
UIAppFonts
AntDesign.ttf
@@ -105,13 +94,25 @@
SUIT-SemiBold.ttf
SUIT-Thin.ttf
- NMFClientId
- j6vmx4vlbe
- NSLocationAlwaysAndWhenInUseUsageDescription
- 내 위치 정보 표시와 산책 기능 제공을 위해 권한 허용이 필요합니다. 설정에서 언제든 변경이 가능합니다.
- NSLocationTemporaryUsageDescriptionDictionary
- 내 위치 정보 표시와 산책 기능 제공을 위해 권한 허용이 필요합니다. 설정에서 언제든 변경이 가능합니다.
- NSLocationWhenInUseUsageDescription
- 내 위치 정보 표시와 산책 기능 제공을 위해 권한 허용이 필요합니다. 설정에서 언제든 변경이 가능합니다.
+ UIBackgroundModes
+
+ fetch
+ processing
+ location
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UIViewControllerBasedStatusBarAppearance
+
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index b476ef7..129c7c0 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -1269,6 +1269,27 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
+ - react-native-cameraroll (7.9.0):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.01.01.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
- react-native-config (1.5.5):
- react-native-config/App (= 1.5.5)
- react-native-config/App (1.5.5):
@@ -1430,6 +1451,27 @@ PODS:
- Yoga
- react-native-splash-screen (3.3.0):
- React-Core
+ - react-native-view-shot (4.0.3):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.01.01.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
- react-native-webview (13.13.2):
- DoubleConversion
- glog
@@ -2051,6 +2093,7 @@ DEPENDENCIES:
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
- React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
+ - "react-native-cameraroll (from `../node_modules/@react-native-camera-roll/camera-roll`)"
- react-native-config (from `../node_modules/react-native-config`)
- react-native-date-picker (from `../node_modules/react-native-date-picker`)
- react-native-encrypted-storage (from `../node_modules/react-native-encrypted-storage`)
@@ -2059,6 +2102,7 @@ DEPENDENCIES:
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- "react-native-slider (from `../node_modules/@react-native-community/slider`)"
- react-native-splash-screen (from `../node_modules/react-native-splash-screen`)
+ - react-native-view-shot (from `../node_modules/react-native-view-shot`)
- react-native-webview (from `../node_modules/react-native-webview`)
- React-nativeconfig (from `../node_modules/react-native/ReactCommon`)
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
@@ -2181,6 +2225,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon"
React-microtasksnativemodule:
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
+ react-native-cameraroll:
+ :path: "../node_modules/@react-native-camera-roll/camera-roll"
react-native-config:
:path: "../node_modules/react-native-config"
react-native-date-picker:
@@ -2197,6 +2243,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-community/slider"
react-native-splash-screen:
:path: "../node_modules/react-native-splash-screen"
+ react-native-view-shot:
+ :path: "../node_modules/react-native-view-shot"
react-native-webview:
:path: "../node_modules/react-native-webview"
React-nativeconfig:
@@ -2314,6 +2362,7 @@ SPEC CHECKSUMS:
React-logger: ae95f0effa7e1791bd6f7283caddca323d4fbc1e
React-Mapbuffer: 7eb5d69e1154e7743487ef0c8d7261e5b59afb32
React-microtasksnativemodule: 01dd998649ff5f8814846b7eee84c4d57f5d3671
+ react-native-cameraroll: 31e39d303319ba20657808f33104afa9d52d83aa
react-native-config: 644074ab88db883fcfaa584f03520ec29589d7df
react-native-date-picker: 26cdb1a94ec72dbc9210c3379e57ff6ba8bc73f2
react-native-encrypted-storage: 569d114e329b1c2c2d9f8c84bcdbe4478dda2258
@@ -2322,6 +2371,7 @@ SPEC CHECKSUMS:
react-native-safe-area-context: 5e53e2b0fc3a2994ad0c89a2486e545b6566d8c4
react-native-slider: d1a9121980fc81678c6d30b82f312c778fba563c
react-native-splash-screen: 95994222cc95c236bd3cdc59fe45ed5f27969594
+ react-native-view-shot: 60117a0ac87a504a39ee9f079e632011916e7724
react-native-webview: dd55f7d3b8c0dcdf8835a44ffc5bd03303dffb30
React-nativeconfig: f7ab6c152e780b99a8c17448f2d99cf5f69a2311
React-NativeModulesApple: 9aeb901b9bfcc9235e912445fb3cf4780a99baf4
diff --git a/package-lock.json b/package-lock.json
index 0a6d585..5ee84b2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,13 +12,16 @@
"@emotion/react": "^11.14.0",
"@mj-studio/react-native-naver-map": "^2.2.0",
"@react-native-async-storage/async-storage": "^2.1.1",
+ "@react-native-camera-roll/camera-roll": "^7.9.0",
"@react-native-community/geolocation": "^3.4.0",
"@react-native-community/netinfo": "^11.4.1",
"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "^7.0.14",
"@react-navigation/native-stack": "^7.2.0",
+ "@react-navigation/stack": "^7.1.1",
"@stomp/stompjs": "^7.0.0",
"@tanstack/react-query": "^5.63.0",
+ "axios": "^1.7.9",
"d3": "^7.9.0",
"fast-text-encoding": "^1.0.6",
"jotai": "^2.11.1",
@@ -44,6 +47,7 @@
"react-native-toast-message": "^2.2.1",
"react-native-url-polyfill": "^2.0.0",
"react-native-vector-icons": "^10.2.0",
+ "react-native-view-shot": "^4.0.3",
"react-native-webview": "^13.13.2",
"sockjs-client": "^1.6.1"
},
@@ -2187,7 +2191,6 @@
"version": "2.0.17",
"resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
"integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==",
- "dev": true,
"dependencies": {
"@types/hammerjs": "^2.0.36"
},
@@ -3676,6 +3679,18 @@
"react-native": "^0.0.0-0 || >=0.65 <1.0"
}
},
+ "node_modules/@react-native-camera-roll/camera-roll": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/@react-native-camera-roll/camera-roll/-/camera-roll-7.9.0.tgz",
+ "integrity": "sha512-Ra0lB1G2H11MzL5aIH3bwlxU1zaHGSZeRs4lBXLBO64Ai1gUgZPR7TYgKDeeRPzNPtSbZKmzs+fuZ/7XoCf1SA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 18.17.0"
+ },
+ "peerDependencies": {
+ "react-native": ">=0.59"
+ }
+ },
"node_modules/@react-native-community/cli": {
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-15.0.1.tgz",
@@ -4373,6 +4388,24 @@
"nanoid": "3.3.8"
}
},
+ "node_modules/@react-navigation/stack": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/@react-navigation/stack/-/stack-7.1.1.tgz",
+ "integrity": "sha512-CBTKQlIkELp05zRiTAv5Pa7OMuCpKyBXcdB3PGMN2Mm55/5MkDsA1IaZorp/6TsVCdllITD6aTbGX/HA/88A6w==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-navigation/elements": "^2.2.5",
+ "color": "^4.2.3"
+ },
+ "peerDependencies": {
+ "@react-navigation/native": "^7.0.14",
+ "react": ">= 18.2.0",
+ "react-native": "*",
+ "react-native-gesture-handler": ">= 2.0.0",
+ "react-native-safe-area-context": ">= 4.0.0",
+ "react-native-screens": ">= 4.0.0"
+ }
+ },
"node_modules/@sideway/address": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
@@ -5596,8 +5629,7 @@
"node_modules/@types/hammerjs": {
"version": "2.0.46",
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz",
- "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==",
- "dev": true
+ "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw=="
},
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
@@ -6776,6 +6808,12 @@
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
},
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -6791,6 +6829,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/axios": {
+ "version": "1.7.9",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
+ "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"node_modules/babel-core": {
"version": "7.0.0-bridge.0",
"resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz",
@@ -7060,6 +7109,15 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
+ "node_modules/base64-arraybuffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
+ "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -7642,6 +7700,18 @@
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==",
"dev": true
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/command-exists": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz",
@@ -7851,6 +7921,15 @@
"node": ">=4"
}
},
+ "node_modules/css-line-break": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
+ "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
+ "license": "MIT",
+ "dependencies": {
+ "utrie": "^1.0.2"
+ }
+ },
"node_modules/css-select": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
@@ -8529,6 +8608,15 @@
"robust-predicates": "^3.0.2"
}
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -9982,6 +10070,26 @@
"node": ">=0.4.0"
}
},
+ "node_modules/follow-redirects": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
"node_modules/for-each": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.4.tgz",
@@ -9997,6 +10105,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/form-data": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
+ "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@@ -10430,6 +10552,19 @@
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true
},
+ "node_modules/html2canvas": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
+ "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
+ "license": "MIT",
+ "dependencies": {
+ "css-line-break": "^2.1.0",
+ "text-segmentation": "^1.0.3"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@@ -14118,6 +14253,12 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
"node_modules/psl": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
@@ -14418,7 +14559,6 @@
"version": "2.23.0",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.23.0.tgz",
"integrity": "sha512-xtkdIU4S4uc4J2WO4hy7AXxD/1M8Be2yOrLdPTuWKAOF3KyL0D0xSdvuaWhI+GdZCNQQisj9kvbnMQGGb9XZNQ==",
- "dev": true,
"dependencies": {
"@egjs/hammerjs": "^2.0.17",
"hoist-non-react-statics": "^3.3.0",
@@ -14766,6 +14906,19 @@
"node": ">=10"
}
},
+ "node_modules/react-native-view-shot": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/react-native-view-shot/-/react-native-view-shot-4.0.3.tgz",
+ "integrity": "sha512-USNjYmED7C0me02c1DxKA0074Hw+y/nxo+xJKlffMvfUWWzL5ELh/TJA/pTnVqFurIrzthZDPtDM7aBFJuhrHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "html2canvas": "^1.4.1"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/react-native-webview": {
"version": "13.13.2",
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.13.2.tgz",
@@ -16420,6 +16573,15 @@
"node": "*"
}
},
+ "node_modules/text-segmentation": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
+ "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
+ "license": "MIT",
+ "dependencies": {
+ "utrie": "^1.0.2"
+ }
+ },
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -16828,6 +16990,15 @@
"node": ">= 0.4.0"
}
},
+ "node_modules/utrie": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
+ "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-arraybuffer": "^1.0.2"
+ }
+ },
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
diff --git a/package.json b/package.json
index 2011554..498d1c1 100644
--- a/package.json
+++ b/package.json
@@ -15,13 +15,16 @@
"@emotion/react": "^11.14.0",
"@mj-studio/react-native-naver-map": "^2.2.0",
"@react-native-async-storage/async-storage": "^2.1.1",
+ "@react-native-camera-roll/camera-roll": "^7.9.0",
"@react-native-community/geolocation": "^3.4.0",
"@react-native-community/netinfo": "^11.4.1",
"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "^7.0.14",
"@react-navigation/native-stack": "^7.2.0",
+ "@react-navigation/stack": "^7.1.1",
"@stomp/stompjs": "^7.0.0",
"@tanstack/react-query": "^5.63.0",
+ "axios": "^1.7.9",
"d3": "^7.9.0",
"fast-text-encoding": "^1.0.6",
"jotai": "^2.11.1",
@@ -47,6 +50,7 @@
"react-native-toast-message": "^2.2.1",
"react-native-url-polyfill": "^2.0.0",
"react-native-vector-icons": "^10.2.0",
+ "react-native-view-shot": "^4.0.3",
"react-native-webview": "^13.13.2",
"sockjs-client": "^1.6.1"
},
diff --git a/src/App.tsx b/src/App.tsx
index a1ad81f..a0f2312 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -5,8 +5,6 @@ import { AppProviders } from '~providers/AppProviders';
import { lightTheme } from '~styles/theme';
import StoryBookUI from '../.storybook';
import { RootNavigator } from '~navigation/RootNavigator';
-import { useWebSocket } from '~hooks/useWebSocket';
-import { WebSocketProvider } from '~providers/WebSocketProvider';
import 'react-native-url-polyfill/auto';
import 'fast-text-encoding';
@@ -20,16 +18,12 @@ const navTheme = {
};
const MainApp = () => {
- const { client, sendMessage } = useWebSocket();
-
return (
-
-
-
-
-
-
-
+
+
+
+
+
);
};
diff --git a/src/apis/walk/completeWalk.ts b/src/apis/walk/completeWalk.ts
new file mode 100644
index 0000000..f1744aa
--- /dev/null
+++ b/src/apis/walk/completeWalk.ts
@@ -0,0 +1,128 @@
+import { HTTPError } from 'ky';
+import { APIResponse } from '~types/api';
+import { api } from '~apis/api';
+
+export interface RequestCompleteWalk {
+ request: {
+ totalDistanceMeter: number;
+ totalWalkTimeSecond: number;
+ };
+ walkImgFile: string;
+}
+
+export interface ResponseCompleteWalk {
+ code: string;
+ status: string;
+ message: string;
+ data: {
+ date: string;
+ memberName: string;
+ dogName: string;
+ totalDistanceMeter: number;
+ timeDuration: {
+ hours: number;
+ minutes: number;
+ seconds: number;
+ };
+ totalCalorie: number;
+ walkImg: string;
+ walkWithDogInfo: {
+ dogId: number;
+ dogProfileImg: string;
+ dogName: string;
+ breed: string;
+ dogAge: number;
+ dogGender: string;
+ memberId: number;
+ };
+ };
+}
+
+const processImageForFormData = (imageUri: string): any => {
+ console.log('[API] 이미지 처리 시작, URI 유형:', typeof imageUri);
+
+ if (!imageUri) {
+ console.error('[API] 이미지 URI가 없음');
+ throw new Error('이미지 URI가 제공되지 않았습니다.');
+ }
+
+ try {
+ if (imageUri.startsWith('file://')) {
+ console.log('[API] 로컬 파일 URI 감지:', imageUri.substring(0, 40) + '...');
+ return {
+ uri: imageUri,
+ name: `walk-map-${Date.now()}.png`,
+ type: 'image/png',
+ };
+ }
+
+ if (imageUri.startsWith('data:image/')) {
+ console.log('[API] Data URI 감지 (base64)');
+ return {
+ uri: imageUri,
+ name: `walk-map-${Date.now()}.png`,
+ type: imageUri.split(';')[0].replace('data:', ''),
+ };
+ }
+
+ console.log('[API] 기타 형식의 URI, 기본 처리 적용');
+ return {
+ uri: imageUri,
+ name: `walk-map-${Date.now()}.png`,
+ type: 'image/png',
+ };
+ } catch (error) {
+ console.error('[API] 이미지 처리 실패:', error);
+ throw new Error('이미지 변환에 실패했습니다: ' + (error instanceof Error ? error.message : '알 수 없는 오류'));
+ }
+};
+
+export const completeWalk = async (userInfo: RequestCompleteWalk): Promise> => {
+ try {
+ console.log('[API] completeWalk 요청 시작:', userInfo);
+
+ const formData = new FormData();
+
+ const mapImageFile = processImageForFormData(userInfo.walkImgFile);
+ console.log('[API] 처리된 이미지 파일:', mapImageFile.name);
+ formData.append('walkImgFile', mapImageFile);
+
+ const requestData = JSON.stringify({
+ totalDistanceMeter: userInfo.request.totalDistanceMeter,
+ totalWalkTimeSecond: userInfo.request.totalWalkTimeSecond,
+ });
+
+ console.log('[API] JSON 요청 데이터:', requestData);
+ formData.append('request', requestData);
+
+ console.log('[API] FormData 구성 완료');
+
+ console.log('[API] API 요청 시작');
+
+ try {
+ const response = await api
+ .post('walk/complete', {
+ body: formData,
+ headers: {
+ Accept: 'application/json',
+ },
+ timeout: 30000, // 30초 타임아웃 (파일 업로드는 시간이 더 걸릴 수 있음)
+ })
+ .json>();
+
+ console.log('[API] completeWalk 응답 성공:', response);
+ return response;
+ } catch (error) {
+ if (error instanceof HTTPError) {
+ const errorData = await error.response.json();
+ console.error('[API] HTTP 오류:', errorData);
+ } else {
+ console.error('[API] 알 수 없는 오류:', error);
+ }
+ throw error;
+ }
+ } catch (error) {
+ console.error('[API] completeWalk 오류:', error);
+ throw error;
+ }
+};
diff --git a/src/apis/walk/startWalk.ts b/src/apis/walk/startWalk.ts
new file mode 100644
index 0000000..4897024
--- /dev/null
+++ b/src/apis/walk/startWalk.ts
@@ -0,0 +1,33 @@
+import { HTTPError } from 'ky';
+import { APIResponse } from '~types/api';
+import { api } from '~apis/api';
+
+export interface RequestStartWalk {
+ dogIds: number[];
+}
+
+export interface ResponseStartWalk {
+ code: string;
+ status: string;
+ message: string;
+ data: {} | string;
+}
+
+export const startWalk = async (userInfo: RequestStartWalk): Promise> => {
+ try {
+ const response = api
+ .post('walk/start', {
+ json: userInfo,
+ })
+ .json>();
+ return response;
+ } catch (error) {
+ if (error instanceof HTTPError) {
+ const errorData = await error.response.json();
+ console.error(errorData);
+ } else {
+ console.error(error);
+ }
+ throw error;
+ }
+};
diff --git a/src/assets/avatars/Avatar1.png b/src/assets/avatars/Avatar1.png
new file mode 100644
index 0000000..a567eec
Binary files /dev/null and b/src/assets/avatars/Avatar1.png differ
diff --git a/src/assets/walk-summary.svg b/src/assets/walk-summary.svg
new file mode 100644
index 0000000..444d580
--- /dev/null
+++ b/src/assets/walk-summary.svg
@@ -0,0 +1,20 @@
+
diff --git a/src/components/Common/Icons/index.tsx b/src/components/Common/Icons/index.tsx
index 64d40f4..5238696 100644
--- a/src/components/Common/Icons/index.tsx
+++ b/src/components/Common/Icons/index.tsx
@@ -33,6 +33,7 @@ import FamilyInvitationGuide from '~assets/family-invitation-guide.svg';
import FamilyJoinGuide from '~assets/family-join-guide.svg';
import FamilyJoinGuide2 from '~assets/family-join-guide2.svg';
import FamilyJoinGuide3 from '~assets/family-join-guide3.svg';
+import WalkSummary from '~assets/walk-summary.svg';
import DogTurnedBack from '~assets/dogs/dog-turned-back.svg';
import Crown from '~assets/crown.svg';
@@ -71,6 +72,7 @@ export const Icon = {
FamilyJoinGuide: (props: SvgProps) => ,
FamilyJoinGuide2: (props: SvgProps) => ,
FamilyJoinGuide3: (props: SvgProps) => ,
+ WalkSummary: (props: SvgProps) => ,
DogTurnedBack: (props: SvgProps) => ,
Crown: (props: SvgProps) => ,
};
diff --git a/src/components/Common/ListModal/index.tsx b/src/components/Common/ListModal/index.tsx
index 339be19..c1f9ace 100644
--- a/src/components/Common/ListModal/index.tsx
+++ b/src/components/Common/ListModal/index.tsx
@@ -1,37 +1,55 @@
import { Modal, ScrollView, Animated, Dimensions, GestureResponderEvent } from 'react-native';
-import { useEffect, useRef } from 'react';
+import { useEffect, useRef, useState } from 'react';
import { Separator } from '~components/Common/Seperator';
import * as S from './styles';
+interface Dog {
+ dogId: number;
+ dogName: string;
+ breed: string;
+ dogBirthDate: string;
+ dogGender: string;
+ walkCount: number;
+ dogProfileImg: string;
+}
+
interface DogListModalProps {
isVisible: boolean;
onClose: () => void;
- dogs: {
- id: string;
- name: string;
- breed: string;
- age: string;
- gender: string;
- walkCount: number;
- imageUrl: string;
- }[];
- onSelectDog: (dog: {
- id: string;
- name: string;
- breed: string;
- age: string;
- gender: string;
- walkCount: number;
- imageUrl: string;
- }) => void;
- type?: 'walk' | 'default' | 'select';
+ dogs: Dog[];
+ onSelectDog: (dog: Dog) => void;
+ onSelectMultipleDogs?: (dogs: Dog[]) => void;
+ type?: 'walk' | 'default' | 'multi-select' | 'select';
}
const SCREEN_HEIGHT = Dimensions.get('window').height;
const WALK_MODAL_HEIGHT = SCREEN_HEIGHT * 0.85;
-export const DogListModal = ({ isVisible, onClose, dogs, onSelectDog, type = 'default' }: DogListModalProps) => {
+const calculateAge = (birthDate: string): number => {
+ const birth = new Date(birthDate);
+ const today = new Date();
+ let age = today.getFullYear() - birth.getFullYear();
+ const monthDiff = today.getMonth() - birth.getMonth();
+
+ if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
+ age--;
+ }
+
+ return age;
+};
+
+export const DogListModal = ({
+ isVisible,
+ onClose,
+ dogs,
+ onSelectDog,
+ onSelectMultipleDogs,
+ type = 'default',
+}: DogListModalProps) => {
const slideAnim = useRef(new Animated.Value(type === 'walk' ? WALK_MODAL_HEIGHT : 500)).current;
+ const [selectedDogs, setSelectedDogs] = useState([]);
+
+ console.log(dogs);
useEffect(() => {
if (isVisible) {
@@ -52,6 +70,23 @@ export const DogListModal = ({ isVisible, onClose, dogs, onSelectDog, type = 'de
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isVisible, type]);
+ const toggleDogSelection = (dog: Dog) => {
+ setSelectedDogs(prevSelected => {
+ if (prevSelected.some(selectedDog => selectedDog.dogId === dog.dogId)) {
+ return prevSelected.filter(selectedDog => selectedDog.dogId !== dog.dogId);
+ } else {
+ return [...prevSelected, dog];
+ }
+ });
+ };
+
+ const handleConfirmSelection = () => {
+ if (onSelectMultipleDogs) {
+ onSelectMultipleDogs(selectedDogs);
+ }
+ onClose();
+ };
+
return (
@@ -65,29 +100,47 @@ export const DogListModal = ({ isVisible, onClose, dogs, onSelectDog, type = 'de
>
{type === 'walk' ? '강번따 리스트' : '강아지 리스트'}
+ {type === 'multi-select' && (
+
+ 선택 완료
+
+ )}
{dogs.map(dog => (
-
+ selectedDog.dogId === dog.dogId)
+ ? 'lightgray'
+ : 'white',
+ }}
+ >
- {dog.name}
+ {dog.dogName}
{dog.breed}
- {dog.age}
+ {calculateAge(dog.dogBirthDate)}살
- {dog.gender}
+ {dog.dogGender === 'MALE' ? '남' : '여'}
산책 횟수 {dog.walkCount}회
onSelectDog(dog)}
+ onPress={() => {
+ if (type === 'multi-select') {
+ toggleDogSelection(dog);
+ } else {
+ onSelectDog(dog);
+ }
+ }}
type="roundedRect"
bgColor="lighten_3"
text={type === 'walk' ? '강번따' : type === 'select' ? '선택' : '추가'}
diff --git a/src/components/Common/ListModal/styles.ts b/src/components/Common/ListModal/styles.ts
index e9cf45a..a202444 100644
--- a/src/components/Common/ListModal/styles.ts
+++ b/src/components/Common/ListModal/styles.ts
@@ -11,7 +11,7 @@ export const ModalBackground = styled.TouchableOpacity`
background-color: rgba(0, 0, 0, 0.5);
`;
-export const ModalContainer = styled(Animated.View)<{ type: 'walk' | 'default' | 'select' }>`
+export const ModalContainer = styled(Animated.View)<{ type: 'walk' | 'default' | 'multi-select' | 'select' }>`
position: absolute;
bottom: 0;
left: 0;
@@ -74,3 +74,26 @@ export const InfoContainer = styled.View`
export const WalkText = styled(TextBold)``;
export const SelectButton = styled(ResizeButton)``;
+
+interface ConfirmButtonProps {
+ bgColor?: string;
+}
+
+export const ConfirmButton = styled.TouchableOpacity`
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ background-color: ${({ theme, bgColor }) => (bgColor === 'default' ? theme.colors.default : theme.colors.lighten_3)};
+ padding: 5px 10px;
+ border-radius: 5px;
+ align-items: center;
+ justify-content: center;
+ width: 70px;
+ height: 40px;
+`;
+
+export const ConfirmButtonText = styled.Text`
+ color: ${({ theme }) => theme.colors.font_1};
+ font-size: 14px;
+ font-weight: bold;
+`;
diff --git a/src/components/Walk/Header/index.tsx b/src/components/Walk/Header/index.tsx
index 062f8cb..f090047 100644
--- a/src/components/Walk/Header/index.tsx
+++ b/src/components/Walk/Header/index.tsx
@@ -1,10 +1,16 @@
import { useNavigation } from '@react-navigation/native';
import * as S from './styles';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import React from 'react';
+import { getAvatar } from '~utils/getAvatar';
+import { useUser } from '~apis/member/useUser';
const WalkHeader = () => {
const navigation = useNavigation();
const insets = useSafeAreaInsets();
+ const avatars = getAvatar();
+ const user = useUser();
+ const AvatarComponent = avatars[user.memberProfileImg];
return (
@@ -17,10 +23,8 @@ const WalkHeader = () => {
navigation.goBack()}>
←
- 강남구 논현동
-
-
-
+ {user.address}
+ {AvatarComponent && }
);
diff --git a/src/components/Walk/MapView/index.tsx b/src/components/Walk/MapView/index.tsx
index 4ede62a..61e1b13 100644
--- a/src/components/Walk/MapView/index.tsx
+++ b/src/components/Walk/MapView/index.tsx
@@ -1,12 +1,100 @@
-import { NaverMapView } from '@mj-studio/react-native-naver-map';
-import { useEffect, useState } from 'react';
+import {
+ Camera,
+ NaverMapMarkerOverlay,
+ NaverMapView,
+ NaverMapViewRef,
+ NaverMapCircleOverlay,
+ NaverMapPathOverlay,
+} from '@mj-studio/react-native-naver-map';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { Alert, Linking, Platform } from 'react-native';
+import Geolocation from '@react-native-community/geolocation';
+import { request, PERMISSIONS, requestLocationAccuracy, requestMultiple } from 'react-native-permissions';
import { formatDuration, formatDistance } from '~screens/Home/WalkScreen';
import * as S from './styles';
+import { useMyDogInfo } from '~apis/dog/useMyDogInfo';
+import { DogListModal } from '~components/Common/ListModal';
+import axios from 'axios';
+
+import ViewShot, { captureRef } from 'react-native-view-shot';
+import { CameraRoll } from '@react-native-camera-roll/camera-roll';
+import WalkSummaryModal from '../WalkSummary';
+import { startWalk } from '~apis/walk/startWalk';
+import { completeWalk } from '~apis/walk/completeWalk';
+import { useWebSocket } from '~hooks/useWebSocket';
+import React from 'react';
+import { getAvatar } from '~utils/getAvatar';
+import { useUser } from '~apis/member/useUser';
+
+const WALKING_INTERVAL = 5000;
+const MIN_ACCURACY = 30;
+const MIN_MARKER_DISTANCE = 20;
+const ROUTE_API_URL = 'https://ruehan-home.com:8004/ors/v2/directions/foot-walking/geojson';
+
+const calculateDirectDistance = (lat1: number, lon1: number, lat2: number, lon2: number): number => {
+ const R = 6371e3;
+ const φ1 = (lat1 * Math.PI) / 180;
+ const φ2 = (lat2 * Math.PI) / 180;
+ const Δφ = ((lat2 - lat1) * Math.PI) / 180;
+ const Δλ = ((lon2 - lon1) * Math.PI) / 180;
+
+ const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+
+ return R * c;
+};
const MapView = () => {
+ const myDogInfo = useMyDogInfo();
+ const { sendMessage, responseData, connectionStatus } = useWebSocket();
+ const avatars = getAvatar();
+ const user = useUser();
+ const AvatarComponent = avatars[user.memberProfileImg];
+
+ console.log('[Walk] 유저 정보:', user);
+
+ useEffect(() => {
+ if (responseData) {
+ console.log('[Walk] WebSocket 응답 데이터 수신:', responseData);
+ }
+ }, [responseData]);
+
+ console.log('[Walk] WebSocket 연결 상태:', connectionStatus);
+
+ const mapRef = useRef(null);
const [isWalking, setIsWalking] = useState(false);
const [walkTime, setWalkTime] = useState(0);
- const [distance, _setDistance] = useState(0);
+ const [distance, setDistance] = useState(0);
+ const [camera, _setCamera] = useState({
+ latitude: 37.50497126,
+ longitude: 127.04905021,
+ zoom: 18,
+ });
+
+ const [currentLocation, setCurrentLocation] = useState<{
+ latitude: number;
+ longitude: number;
+ }>({
+ latitude: 37.50497126,
+ longitude: 127.04905021,
+ });
+ const previousLocationRef = useRef<{ latitude: number; longitude: number } | null>(null);
+ const [locationMarkers, setLocationMarkers] = useState<
+ {
+ latitude: number;
+ longitude: number;
+ index: number;
+ }[]
+ >([]);
+ const lastUpdateTimeRef = useRef(Date.now());
+
+ const [isLocationCentered, setIsLocationCentered] = useState(true);
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const [routeCoordinates, setRouteCoordinates] = useState([]);
+ const viewShotRef = useRef(null);
+ const [screenshotUri, setScreenshotUri] = useState('');
+ const [isWalkSummaryVisible, setIsWalkSummaryVisible] = useState(false);
+ const [locationPermissionGranted, setLocationPermissionGranted] = useState(false);
useEffect(() => {
let interval: NodeJS.Timeout;
@@ -17,35 +105,628 @@ const MapView = () => {
}
return () => clearInterval(interval);
}, [isWalking]);
+ const filterPosition = (position: { coords: { accuracy: number; latitude: number; longitude: number } }): boolean => {
+ const isAccurate = position.coords.accuracy <= MIN_ACCURACY;
+ if (!isAccurate) {
+ console.log('[Walk] 위치 정확도 낮음, 무시됨:', position.coords.accuracy);
+ }
+ return isAccurate;
+ };
+ const shouldAddMarker = useCallback(
+ (newPosition: { latitude: number; longitude: number }): boolean => {
+ if (locationMarkers.length === 0) {
+ console.log('[Walk] 첫 마커 추가');
+ return true;
+ }
+
+ const lastMarker = locationMarkers[locationMarkers.length - 1];
+ const calDistance = calculateDirectDistance(
+ lastMarker.latitude,
+ lastMarker.longitude,
+ newPosition.latitude,
+ newPosition.longitude,
+ );
+
+ const shouldAdd = calDistance >= MIN_MARKER_DISTANCE;
+ console.log('[Walk] 마커 추가 여부:', shouldAdd, '거리:', calDistance.toFixed(2) + 'm');
+ return shouldAdd;
+ },
+ [locationMarkers],
+ );
+ const requestLocationPermission = useCallback(async () => {
+ try {
+ if (Platform.OS === 'ios') {
+ const status = await request(PERMISSIONS.IOS.LOCATION_ALWAYS);
+ console.log('[Walk] iOS 위치 권한 상태:', status);
+
+ if (status === 'granted') {
+ try {
+ await requestLocationAccuracy({ purposeKey: 'common-purpose' });
+ setLocationPermissionGranted(true);
+ } catch (e) {
+ console.error('[Walk] iOS 위치 정확도 요청 실패:', e);
+ Alert.alert('위치 정확도 설정 필요', '정확한 위치 정보가 필요합니다. 설정에서 권한을 변경해주세요.', [
+ { text: '취소', style: 'cancel' },
+ {
+ text: '설정으로 이동',
+ onPress: () => {
+ Linking.openURL('app-settings:');
+ },
+ },
+ ]);
+ }
+ } else if (status === 'denied' || status === 'blocked') {
+ Alert.alert(
+ '위치 권한이 필요합니다',
+ '산책 기능을 사용하려면 위치 권한이 필요합니다. 설정에서 위치 권한을 허용해주세요.',
+ [
+ { text: '취소', style: 'cancel' },
+ {
+ text: '설정으로 이동',
+ onPress: () => {
+ Linking.openURL('app-settings:');
+ },
+ },
+ ],
+ );
+ } else {
+ Alert.alert('위치 권한 필요', '산책 기능을 사용하려면 항상 또는 앱 사용 중 위치 권한이 필요합니다.');
+ }
+ } else {
+ const statuses = await requestMultiple([
+ PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION,
+ PERMISSIONS.ANDROID.ACCESS_BACKGROUND_LOCATION,
+ ]);
+ console.log('[Walk] Android 위치 권한 상태:', statuses);
+
+ if (statuses[PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION] === 'granted') {
+ setLocationPermissionGranted(true);
+ } else if (
+ statuses[PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION] === 'denied' ||
+ statuses[PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION] === 'blocked'
+ ) {
+ Alert.alert(
+ '위치 권한이 필요합니다',
+ '산책 기능을 사용하려면 위치 권한이 필요합니다. 설정에서 위치 권한을 허용해주세요.',
+ [
+ { text: '취소', style: 'cancel' },
+ {
+ text: '설정으로 이동',
+ onPress: () => {
+ Linking.openSettings();
+ },
+ },
+ ],
+ );
+ } else {
+ Alert.alert('위치 권한 필요', '산책 기능을 사용하려면 위치 권한이 필요합니다.');
+ }
+ }
+ } catch (e) {
+ console.error('[Walk] 위치 권한 요청 실패:', e);
+ Alert.alert('오류', '위치 권한을 요청하는 중 오류가 발생했습니다.');
+ }
+ }, []);
+
+ useEffect(() => {
+ const initializeLocation = async () => {
+ await requestLocationPermission();
+
+ Geolocation.getCurrentPosition(
+ position => {
+ const { latitude, longitude } = position.coords;
+ console.log('[Walk] 초기 위치 가져오기 성공:', { latitude, longitude });
+
+ setCurrentLocation({ latitude, longitude });
+
+ mapRef.current?.animateCameraTo({
+ latitude,
+ longitude,
+ zoom: 18,
+ duration: 500,
+ easing: 'Fly',
+ });
+ },
+ error => {
+ console.error('[Walk] 초기 위치 가져오기 실패:', error);
+ Alert.alert('위치 오류', '현재 위치를 가져올 수 없습니다. 위치 서비스가 활성화되어 있는지 확인해주세요.');
+ },
+ {
+ enableHighAccuracy: true,
+ timeout: 5000,
+ },
+ );
+ };
+
+ initializeLocation();
+ }, [requestLocationPermission]);
+
+ useEffect(() => {
+ if (!locationPermissionGranted) {
+ console.log('[Walk] 위치 권한 없음, 추적 중단');
+ return;
+ }
+
+ console.log('[Walk] 위치 추적 시작 (권한 있음)');
+
+ const watchId = Geolocation.watchPosition(
+ position => {
+ const currentTime = Date.now();
+ const timeSinceLastUpdate = currentTime - lastUpdateTimeRef.current;
+
+ console.log('[Walk] 위치 데이터 수신:', {
+ accuracy: position.coords.accuracy,
+ lastUpdate: timeSinceLastUpdate,
+ isWalking: isWalking,
+ coords: {
+ latitude: position.coords.latitude,
+ longitude: position.coords.longitude,
+ },
+ });
+
+ if (timeSinceLastUpdate < WALKING_INTERVAL) {
+ return;
+ }
+
+ if (!filterPosition(position)) {
+ console.log('[Walk] 정확도 불충분, 무시:', position.coords.accuracy);
+ return;
+ }
+
+ const { latitude, longitude } = position.coords;
+ const newPosition = { latitude, longitude };
+
+ previousLocationRef.current = currentLocation;
+ setCurrentLocation(newPosition);
+ lastUpdateTimeRef.current = currentTime;
+
+ console.log('[Walk] 위치 업데이트 완료:', { latitude, longitude });
+
+ if (isWalking) {
+ console.log('[Walk] 산책 중 위치 업데이트 처리');
+
+ if (shouldAddMarker(newPosition)) {
+ console.log('[Walk] 새 마커 추가:', newPosition);
+ setLocationMarkers(prev => [
+ ...prev,
+ {
+ latitude,
+ longitude,
+ index: prev.length + 1,
+ },
+ ]);
+ }
+
+ if (isLocationCentered) {
+ console.log('[Walk] 카메라 위치 이동');
+ mapRef.current?.animateCameraTo({
+ latitude,
+ longitude,
+ zoom: 18,
+ duration: 500,
+ easing: 'Fly',
+ });
+ }
+ }
+ },
+ error => {
+ console.error('[Walk] 위치 추적 오류:', error);
+ Alert.alert('위치 추적 오류', '위치를 추적하는 중 오류가 발생했습니다.');
+ },
+ {
+ enableHighAccuracy: true,
+ distanceFilter: 0,
+ interval: 1000,
+ timeout: 10000, // 타임아웃을 10초로 늘림
+ },
+ );
+
+ return () => {
+ console.log('[Walk] 위치 추적 중지');
+ Geolocation.clearWatch(watchId);
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [locationPermissionGranted, isWalking, isLocationCentered, shouldAddMarker]);
+
+ const handleLocationButtonPress = useCallback(() => {
+ console.log('[Walk] 내 위치로 이동');
+ mapRef.current?.animateCameraTo({
+ latitude: currentLocation.latitude,
+ longitude: currentLocation.longitude,
+ zoom: 18,
+ duration: 500,
+ easing: 'Fly',
+ });
+ setIsLocationCentered(true);
+ }, [currentLocation]);
+
+ const handleStartWalkPress = useCallback(() => {
+ console.log('[Walk] 산책 시작 버튼 클릭');
+
+ if (!locationPermissionGranted) {
+ console.log('[Walk] 위치 권한 없음, 권한 요청 시도');
+ Alert.alert('위치 권한 필요', '산책 기능을 사용하려면 위치 권한이 필요합니다. 권한을 허용하시겠습니까?', [
+ { text: '취소', style: 'cancel' },
+ {
+ text: '권한 요청',
+ onPress: async () => {
+ await requestLocationPermission();
+ if (locationPermissionGranted) {
+ setIsModalVisible(true);
+ }
+ },
+ },
+ ]);
+ return;
+ }
+
+ setIsModalVisible(true);
+ }, [locationPermissionGranted, requestLocationPermission]);
+
+ const handleStopWalkPress = useCallback(async () => {
+ console.log('[Walk] 산책 종료 버튼 클릭');
+ setIsWalking(false);
+
+ try {
+ console.log('[Walk] 스크린샷 촬영 시작');
+ const uri = await captureRef(viewShotRef, {
+ format: 'png',
+ quality: 0.8,
+ });
- const renderWalkButton = () => {
+ const savedUri = await CameraRoll.save(uri, { type: 'photo' });
+ console.log('[Walk] 스크린샷 저장 성공:', savedUri);
+
+ console.log('[Walk] 산책 데이터 준비:', {
+ 시간: walkTime,
+ 거리: distance,
+ 이미지URI: uri.substring(0, 50) + '...',
+ });
+
+ const response = await completeWalk({
+ request: {
+ totalDistanceMeter: distance,
+ totalWalkTimeSecond: walkTime,
+ },
+ walkImgFile: uri,
+ });
+
+ console.log('[Walk] 산책 완료 API 성공:', response);
+
+ setScreenshotUri(savedUri);
+ setIsWalkSummaryVisible(true);
+ } catch (error) {
+ console.error('[Walk] 산책 종료 처리 실패:', error);
+ if (error instanceof Error) {
+ console.error('[Walk] 오류 메시지:', error.message);
+ console.error('[Walk] 오류 스택:', error.stack);
+ }
+ Alert.alert('오류', '산책 종료 처리 중 오류가 발생했습니다.');
+ }
+ }, [walkTime, distance]);
+
+ const handleWalkSummaryClose = useCallback(() => {
+ console.log('[Walk] 산책 요약 닫기');
+ setWalkTime(0);
+ setDistance(0);
+ setLocationMarkers([]);
+ setRouteCoordinates([]);
+ setIsWalkSummaryVisible(false);
+ }, []);
+
+ const handleSelectDog = useCallback(
+ async (dogs: any) => {
+ console.log('[Walk] 선택된 강아지:', dogs);
+ setIsModalVisible(false);
+
+ try {
+ const dogIds = dogs.map((d: any) => d.dogId);
+ console.log('[Walk] 산책 시작 강아지 IDs:', dogIds);
+
+ const response = await startWalk({ dogIds });
+ console.log('[Walk] 산책 시작 API 성공:', response);
+
+ if (
+ !currentLocation ||
+ (currentLocation.latitude === 37.50497126 && currentLocation.longitude === 127.04905021)
+ ) {
+ console.log('[Walk] 현재 위치 업데이트 필요, 위치 가져오기 시도');
+
+ Geolocation.getCurrentPosition(
+ position => {
+ const { latitude, longitude } = position.coords;
+ console.log('[Walk] 산책 시작 전 위치 업데이트 성공:', { latitude, longitude });
+
+ setCurrentLocation({ latitude, longitude });
+
+ mapRef.current?.animateCameraTo({
+ latitude,
+ longitude,
+ zoom: 18,
+ duration: 500,
+ easing: 'Fly',
+ });
+
+ setLocationMarkers([
+ {
+ latitude,
+ longitude,
+ index: 0,
+ },
+ ]);
+
+ setIsWalking(true);
+ setIsLocationCentered(true);
+ setDistance(0);
+ lastProcessedMarkerIndexRef.current = -1;
+ },
+ error => {
+ console.error('[Walk] 산책 시작 위치 가져오기 실패:', error);
+ Alert.alert(
+ '위치 오류',
+ '현재 위치를 가져올 수 없습니다. 위치 서비스가 활성화되어 있는지 확인 후 다시 시도해주세요.',
+ );
+ },
+ {
+ enableHighAccuracy: true,
+ timeout: 10000,
+ },
+ );
+ } else {
+ console.log('[Walk] 현재 위치로 산책 시작:', currentLocation);
+
+ mapRef.current?.animateCameraTo({
+ latitude: currentLocation.latitude,
+ longitude: currentLocation.longitude,
+ zoom: 18,
+ duration: 500,
+ easing: 'Fly',
+ });
+
+ setLocationMarkers([
+ {
+ latitude: currentLocation.latitude,
+ longitude: currentLocation.longitude,
+ index: 0,
+ },
+ ]);
+
+ setIsWalking(true);
+ setIsLocationCentered(true);
+ setDistance(0);
+ lastProcessedMarkerIndexRef.current = -1;
+ }
+ } catch (error) {
+ console.error('[Walk] 산책 시작 실패:', error);
+ Alert.alert('오류', '산책을 시작하는 중 오류가 발생했습니다.');
+ }
+ },
+ [currentLocation],
+ );
+
+ const renderWalkButton = useCallback(() => {
if (!isWalking) {
- return setIsWalking(true)} bgColor="default" text="산책 시작" />;
+ return ;
}
return (
{formatDuration(walkTime)}
- setIsWalking(false)} bgColor="lighten_2" text="산책 끝" />
- {formatDistance(distance)}km
+
+ {formatDistance(distance)}
);
- };
+ }, [isWalking, walkTime, distance, handleStartWalkPress, handleStopWalkPress]);
+
+ const handleCameraChange = useCallback(
+ (event: any) => {
+ const { latitude, longitude } = event;
+
+ const calDistance = calculateDirectDistance(
+ latitude,
+ longitude,
+ currentLocation.latitude,
+ currentLocation.longitude,
+ );
+
+ const centered = calDistance < 20;
+ if (isLocationCentered !== centered) {
+ setIsLocationCentered(centered);
+ console.log('[Walk] 지도 중심 상태 변경:', centered);
+ }
+ },
+ [currentLocation, isLocationCentered],
+ );
+
+ const fetchRouteData = useCallback(
+ async (markers: { longitude: number; latitude: number }[]) => {
+ if (markers.length < 2) {
+ console.log('[Walk] 경로 계산 건너뜀: 마커가 부족함');
+ return;
+ }
+
+ const lastTwoMarkers = markers.slice(-2);
+ const lastTwoCoordinates = lastTwoMarkers.map(marker => [marker.longitude, marker.latitude]);
+
+ console.log('[Walk] 경로 계산 요청:', lastTwoCoordinates);
+
+ try {
+ const response = await axios.post(
+ ROUTE_API_URL,
+ {
+ coordinates: lastTwoCoordinates,
+ },
+ {
+ timeout: 10000,
+ },
+ );
+
+ const routeData = response.data;
+
+ if (
+ !routeData?.features?.[0]?.geometry?.coordinates ||
+ !routeData?.features?.[0]?.properties?.segments?.[0]?.distance
+ ) {
+ console.error('[Walk] 경로 데이터 형식 오류:', routeData);
+ return;
+ }
+
+ const newRouteCoordinates = routeData.features[0].geometry.coordinates;
+ const segmentDistance = routeData.features[0].properties.segments[0].distance;
+
+ console.log('[Walk] 경로 계산 성공:', {
+ 좌표수: newRouteCoordinates.length,
+ 구간거리: segmentDistance,
+ 현재총거리: distance,
+ });
+
+ setRouteCoordinates(prev => {
+ if (
+ prev.length > 0 &&
+ prev[prev.length - 1][0] === newRouteCoordinates[0][0] &&
+ prev[prev.length - 1][1] === newRouteCoordinates[0][1]
+ ) {
+ return [...prev, ...newRouteCoordinates.slice(1)];
+ }
+ return [...prev, ...newRouteCoordinates];
+ });
+
+ if (lastProcessedMarkerIndexRef.current === 2) {
+ console.log('[Walk] 첫 구간 거리 설정:', segmentDistance);
+ setDistance(segmentDistance);
+ } else if (segmentDistance < 1000) {
+ console.log('[Walk] 거리 추가:', distance, '+', segmentDistance, '=', distance + segmentDistance);
+ setDistance(prevDistance => prevDistance + segmentDistance);
+ } else {
+ console.warn('[Walk] 비정상적으로 큰 구간 거리 무시:', segmentDistance);
+ }
+
+ if (connectionStatus === 'connected' && newRouteCoordinates.length > 0) {
+ const lastCoordinate = newRouteCoordinates[newRouteCoordinates.length - 1];
+
+ const message = JSON.stringify({
+ latitude: lastCoordinate[1],
+ longitude: lastCoordinate[0],
+ });
+
+ console.log('[Walk] WebSocket publish 시도 - /pub/api/v1/walk-alone');
+ const sent = sendMessage('/pub/api/v1/walk-alone', message);
+ console.log('[Walk] WebSocket 메시지 전송 결과:', sent);
+
+ if (sent) {
+ console.log('[Walk] 메시지 내용:', JSON.parse(message));
+ } else {
+ console.error('[Walk] WebSocket 메시지 전송 실패');
+ }
+ } else {
+ console.log('[Walk] WebSocket 메시지 전송 건너뜀:', {
+ 연결상태: connectionStatus,
+ 좌표수: newRouteCoordinates ? newRouteCoordinates.length : 0,
+ });
+ }
+ } catch (error) {
+ console.error('[Walk] 경로 데이터 가져오기 실패:', error);
+ if (axios.isAxiosError(error)) {
+ console.error('[Walk] 오류 세부 정보:', {
+ message: error.message,
+ status: error.response?.status,
+ data: error.response?.data,
+ });
+ }
+ }
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [connectionStatus, sendMessage, distance, walkTime],
+ );
+
+ const drawRoutePolygon = useCallback(() => {
+ if (isWalking && routeCoordinates.length >= 2) {
+ console.log('[Walk] 경로 폴리곤 그리기:', routeCoordinates.length, '개 좌표');
+ return (
+ ({ latitude, longitude }))}
+ />
+ );
+ }
+ return null;
+ }, [isWalking, routeCoordinates]);
+
+ const lastProcessedMarkerIndexRef = useRef(-1);
+
+ useEffect(() => {
+ if (isWalking && locationMarkers.length >= 2) {
+ if (locationMarkers.length > lastProcessedMarkerIndexRef.current) {
+ console.log(`[Walk] 새 마커 처리: ${lastProcessedMarkerIndexRef.current + 1} -> ${locationMarkers.length}`);
+ fetchRouteData(locationMarkers);
+ lastProcessedMarkerIndexRef.current = locationMarkers.length;
+ }
+ }
+ }, [locationMarkers, isWalking, fetchRouteData]);
return (
<>
-
+
+
+
+ {AvatarComponent && }
+
+ {locationMarkers.map((marker, index) => (
+
+ ))}
+ {drawRoutePolygon()}
+
+
- {}} text="⊕ 내 위치로" bgColor="font_1" />
+ {!isLocationCentered && (
+
+ )}
{renderWalkButton()}
+
+ {isModalVisible && (
+ setIsModalVisible(false)}
+ dogs={myDogInfo.data}
+ onSelectMultipleDogs={handleSelectDog}
+ type="multi-select"
+ />
+ )}
+
+
>
);
};
diff --git a/src/components/Walk/WalkSummary/index.tsx b/src/components/Walk/WalkSummary/index.tsx
new file mode 100644
index 0000000..5be88a8
--- /dev/null
+++ b/src/components/Walk/WalkSummary/index.tsx
@@ -0,0 +1,58 @@
+import { Text, Image } from 'react-native';
+import * as S from './styles';
+import { Icon } from '~components/Common/Icons';
+
+interface WalkSummaryModalProps {
+ visible: boolean;
+ walkTime: number;
+ distance: number;
+ screenshotUri: string;
+ onClose: () => void;
+}
+
+const WalkSummaryModal = ({ visible, walkTime, distance, screenshotUri }: WalkSummaryModalProps) => {
+ if (!visible) {
+ return null;
+ }
+
+ console.log(walkTime, distance);
+
+ const formattedDate = new Date().toISOString().split('T')[0].replace(/-/g, '.');
+
+ return (
+
+
+
+
+ {formattedDate}
+ {'견주님과 밤톨이가'}
+
+ {`${Math.floor(walkTime / 60)}분`}
+ {'동안 산책했어요.'}
+
+
+
+
+
+ {`${Math.floor(walkTime / 3600)}:${Math.floor((walkTime % 3600) / 60)}:${
+ walkTime % 60
+ }`}
+ 산책 시간
+
+
+ {`${(distance / 1000).toFixed(1)}km`}
+ 산책 거리
+
+
+ 200kcal
+ 소모한 칼로리
+
+
+
+
+
+
+ );
+};
+
+export default WalkSummaryModal;
diff --git a/src/components/Walk/WalkSummary/styles.ts b/src/components/Walk/WalkSummary/styles.ts
new file mode 100644
index 0000000..85e0032
--- /dev/null
+++ b/src/components/Walk/WalkSummary/styles.ts
@@ -0,0 +1,70 @@
+import styled from '@emotion/native';
+import { BgBox } from '~components/Common/BgBox';
+import { TextBold, TextExtraBold, TextMedium } from '~components/Common/Text';
+
+export const SafeAreaView = styled.SafeAreaView`
+ flex: 1;
+ justify-content: flex-end;
+ align-items: center;
+ pointer-events: box-none;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 1000;
+ background-color: ${({ theme }) => theme.colors.lighten_3};
+`;
+
+export const Container = styled.View`
+ width: 100%;
+ height: 100%;
+ padding: 20px;
+ elevation: 5;
+`;
+
+export const Content = styled.View`
+ align-items: center;
+`;
+
+export const Header = styled.View`
+ width: 100%;
+ margin-bottom: 20px;
+ align-items: start;
+`;
+
+export const Date = styled(TextBold)`
+ margin-bottom: 23px;
+ color: ${({ theme }) => theme.colors.font_1};
+`;
+
+export const Title = styled(TextExtraBold)`
+ margin-bottom: 10px;
+ color: ${({ theme }) => theme.colors.font_1};
+`;
+
+export const Minute = styled(TextExtraBold)`
+ color: ${({ theme }) => theme.colors.default};
+`;
+
+export const InfoContainer = styled(BgBox)`
+ margin-top: 25px;
+ margin-bottom: 20px;
+ flex-direction: row;
+ justify-content: space-around;
+ width: 100%;
+`;
+
+export const InfoMain = styled(TextExtraBold)`
+ flex-direction: row;
+ align-items: center;
+`;
+
+export const InfoSub = styled(TextMedium)`
+ flex-direction: row;
+ align-items: center;
+`;
+
+export const InfoItem = styled.View`
+ align-items: center; // 중앙 정렬
+`;
diff --git a/src/hooks/useWebSocket.ts b/src/hooks/useWebSocket.ts
index c44e77b..595aebf 100644
--- a/src/hooks/useWebSocket.ts
+++ b/src/hooks/useWebSocket.ts
@@ -1,47 +1,126 @@
-import { useEffect, useRef } from 'react';
+import { useEffect, useRef, useState } from 'react';
import SockJS from 'sockjs-client';
-import { Client } from '@stomp/stompjs';
-
-const token =
- 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJBY2Nlc3NUb2tlbiIsInByb3ZpZGVyIjoiR09PR0xFIiwiZXhwIjoxNzQwMjQ4MjA3LCJlbWFpbCI6ImJibGJibGFuNjlAZ21haWwuY29tIn0.CpauBw9_yXlYQjr-BZP7xqm1u63pj1g1aM3kX9HwCm37BMhpOQGz1Mq8R42CihtC8henTRy0OHaxa7q9-1Svzw';
+import { Client, IMessage } from '@stomp/stompjs';
+import { getAccessToken } from '~utils/controlAccessToken';
+import { useUser } from '~apis/member/useUser';
const SERVER_URL = 'https://ddang.site/ws';
export const useWebSocket = () => {
const stompClientRef = useRef(null);
+ const [responseData, setResponseData] = useState(null);
+ const [accessToken, setAccessToken] = useState(null);
+ const [connectionStatus, setConnectionStatus] = useState<'disconnected' | 'connecting' | 'connected'>('disconnected');
+ const reconnectAttemptRef = useRef(0);
+
+ const { email } = useUser();
+
+ console.log('[WebSocket] User email:', email);
+ // Token 가져오기
useEffect(() => {
+ const getToken = async () => {
+ const Token = await getAccessToken();
+ console.log('[WebSocket] Token retrieved:', Token ? 'Success' : 'Failed');
+ setAccessToken(Token);
+ };
+ getToken();
+ }, []);
+
+ // WebSocket 연결 설정
+ useEffect(() => {
+ if (!accessToken || accessToken === 'none' || !email) {
+ console.log('[WebSocket] Connection skipped - missing token or email');
+ return;
+ }
+
+ console.log('[WebSocket] Setting up connection with token and email:', email);
+ setConnectionStatus('connecting');
+
const stompClient = new Client({
webSocketFactory: () => new SockJS(SERVER_URL),
reconnectDelay: 5000,
- debug: msg => console.log(msg),
+ debug: process.env.NODE_ENV === 'development' ? msg => console.log('[WebSocket Debug]', msg) : undefined,
connectHeaders: {
- Authorization: `Bearer ${token}`,
+ Authorization: `Bearer ${accessToken}`,
},
});
stompClient.onConnect = frame => {
- console.log('STOMP 연결 성공:', frame);
+ console.log('[WebSocket] Connection established:', frame?.command);
+ setConnectionStatus('connected');
+ reconnectAttemptRef.current = 0;
+
+ // 이메일로 구독 - walk 응답 처리
+ console.log(`[WebSocket] Subscribing to /sub/walk/${email}`);
+ stompClient.subscribe(`/sub/walk/${email}`, message => {
+ try {
+ const response = JSON.parse(message.body);
+ console.log('[WebSocket] Message received from /sub/walk:', response);
- // 메시지 구독 예제
- // stompClient.subscribe('/sub/walk/bblbblan69@gmail.com', message => {
- // console.log('받은 메시지:', message.body);
- // });
+ // 응답 데이터 저장 및 처리
+ setResponseData(prevData => {
+ console.log('[WebSocket] Updating response data:', {
+ previous: prevData,
+ new: response,
+ });
+ return response;
+ });
+ } catch (error) {
+ console.error('[WebSocket] Error parsing message:', error);
+ }
+ });
+ };
+
+ // 연결 오류 처리
+ stompClient.onStompError = frame => {
+ console.error('[WebSocket] Connection error:', frame.headers?.message);
+ setConnectionStatus('disconnected');
+ reconnectAttemptRef.current += 1;
+ };
+
+ stompClient.onWebSocketClose = () => {
+ console.log('[WebSocket] Connection closed');
+ setConnectionStatus('disconnected');
};
stompClient.activate();
stompClientRef.current = stompClient;
return () => {
- stompClient.deactivate();
+ console.log('[WebSocket] Cleaning up connection');
+ if (stompClient.connected) {
+ stompClient.deactivate();
+ }
};
- }, []);
+ }, [accessToken, email]); // email과 accessToken 변경 시 재연결
const sendMessage = (destination: string, body: string) => {
if (stompClientRef.current && stompClientRef.current.connected) {
+ console.log(`[WebSocket] Sending message to ${destination}:`, body);
stompClientRef.current.publish({ destination, body });
+ return true;
+ } else {
+ console.warn('[WebSocket] Cannot send message - not connected');
+ return false;
}
};
- return { client: stompClientRef.current, sendMessage };
+ const subscribe = (destination: string, callback: (message: IMessage) => void) => {
+ if (stompClientRef.current && stompClientRef.current.connected) {
+ console.log(`[WebSocket] Subscribing to ${destination}`);
+ return stompClientRef.current.subscribe(destination, callback);
+ } else {
+ console.warn('[WebSocket] Cannot subscribe - not connected');
+ return undefined;
+ }
+ };
+
+ return {
+ client: stompClientRef.current,
+ sendMessage,
+ subscribe,
+ responseData,
+ connectionStatus,
+ };
};
diff --git a/src/providers/AppProviders.tsx b/src/providers/AppProviders.tsx
index ecca4ed..f77046a 100644
--- a/src/providers/AppProviders.tsx
+++ b/src/providers/AppProviders.tsx
@@ -2,12 +2,15 @@ import { PropsWithChildren } from 'react';
import { EmotionProvider } from '~providers/EmotionProvider';
import { TanstackQueryProvider } from '~providers/QueryClientProvider';
import { ToastProvider } from '~providers/ToastProvider';
+import { WebSocketProvider } from '~providers/WebSocketProvider';
export const AppProviders = ({ children }: PropsWithChildren) => {
return (
- {children}
+
+ {children}
+
);
diff --git a/src/providers/WebSocketProvider.tsx b/src/providers/WebSocketProvider.tsx
index af85047..afd1f28 100644
--- a/src/providers/WebSocketProvider.tsx
+++ b/src/providers/WebSocketProvider.tsx
@@ -1,28 +1,23 @@
import { Client } from '@stomp/stompjs';
import { createContext, useContext, PropsWithChildren } from 'react';
+import { useWebSocket } from '~hooks/useWebSocket';
type WebSocketContextType = {
client: Client | null;
sendMessage: (destination: string, body: any) => void;
};
-const WebSocketContext = createContext({
- client: null,
- sendMessage: () => {}, // 기본 빈 함수
-});
+const WebSocketContext = createContext(undefined);
-type WebSocketProviderProps = {
- client: Client | null;
- sendMessage: (destination: string, body: any) => void;
-};
+export const WebSocketProvider = ({ children }: PropsWithChildren) => {
+ const { client, sendMessage } = useWebSocket();
-export const WebSocketProvider = ({ children, client, sendMessage }: PropsWithChildren) => {
return {children};
};
export const useWebSocketContext = () => {
const context = useContext(WebSocketContext);
- if (context === undefined) {
+ if (!context) {
throw new Error('useWebSocketContext must be used within a WebSocketProvider');
}
return context;
diff --git a/src/screens/Home/WalkScreen.tsx b/src/screens/Home/WalkScreen.tsx
index 3cde54d..7277725 100644
--- a/src/screens/Home/WalkScreen.tsx
+++ b/src/screens/Home/WalkScreen.tsx
@@ -2,18 +2,24 @@ import { useNavigation } from '@react-navigation/native';
import { useLayoutEffect } from 'react';
import WalkHeader from '~components/Walk/Header';
import MapView from '~components/Walk/MapView';
-import WalkMessage from '~components/Walk/WalkMessage';
import { SafeAreaView, View } from 'react-native';
-export const formatDuration = (seconds: number) => {
+export const formatDuration = (seconds: number): string => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
+ const remainingSeconds = seconds % 60;
- return `${hours}시간 ${minutes}분`;
+ return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(remainingSeconds).padStart(
+ 2,
+ '0',
+ )}`;
};
-export const formatDistance = (meters: number) => {
- return (meters / 1000).toFixed(1);
+export const formatDistance = (meters: number): string => {
+ if (meters >= 1000) {
+ return `${(meters / 1000).toFixed(2)}km`;
+ }
+ return `${Math.round(meters)}m`;
};
export const WalkScreen = () => {
@@ -30,7 +36,7 @@ export const WalkScreen = () => {
-
+ {/* */}
);