Feature: 경로 이탈 알림 기능#45
Conversation
- useCourseValidation 훅을 새로 추가하여 실시간 위치 검증 및 코스 이탈 감지 기능 구현 - Run 컴포넌트에서 코스 검증 훅을 사용하여 이탈 상태에 따른 알림 메시지 표시 - run.ts 스토어에 코스 검증 상태 및 관련 액션 추가 - courseValidation.types.ts 및 courseValidation.ts 파일 생성하여 타입 정의 및 검증 로직 구현
- courseId가 변경될 때마다 경로 데이터를 다시 로드하도록 useFocusEffect 내의 로직 수정 - 불필요한 import 제거 및 코드 정리
…Runova_FE into feature/off-the-course
- Run 페이지에 헤더 컴포넌트를 추가하여 사용자 경험 개선 - ControlContainer에서 FloatingButton을 제거하고, 현재 위치 버튼을 추가하여 UI 간소화 - ControlContainer의 props를 통해 현재 위치 버튼 클릭 핸들러 전달
- OnCourseIndicator 컴포넌트를 제거하여 코드 간소화 - DeviationAlert의 위치 조정 및 정렬 방식 변경으로 UI 개선 - 사용하지 않는 import 문 제거
- useLocationTracking 훅에서 위치 관련 데이터를 통합하여 코드 간소화 - 현재 위치 버튼 클릭 시 트래킹 중인 경우 마지막 위치로 이동하도록 로직 수정 - RunMap 컴포넌트에서 위치 마커를 트래킹 중인 마지막 위치로 표시하도록 변경
- useCourseValidation 훅에 트래킹 상태 및 경로 좌표를 추가하여 실시간 위치 검증 로직 개선 - Run 페이지에서 위치 로딩 상태에 따른 UI 업데이트 및 새로고침 기능 추가 - RunMap 컴포넌트에서 실시간 위치 업데이트를 위한 로직 수정 - 코스 이탈 상태에 따른 메시지를 useMemo로 최적화하여 성능 개선
- useInitialLocation 훅에 권한 요청 옵션 추가하여 유연성 향상 - 위치 권한 상태를 체크하고, 필요 시에만 권한 요청하도록 로직 수정 - useLocationManager 및 useLocationTracking 훅에서 초기 위치 설정 및 권한 요청 로직 통합 - 디버깅 로그 추가로 위치 업데이트 및 초기 위치 가져오기 과정에서의 가시성 향상
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
|
/gemini review |
There was a problem hiding this comment.
Summary of Changes
Hello @Monixc, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!
이 PR은 러닝 애플리케이션에 실시간 경로 이탈 알림 기능을 도입하여 사용자가 코스를 벗어났을 때 즉각적인 피드백을 받을 수 있도록 합니다. 지리 공간 계산 라이브러리를 활용하여 현재 위치와 코스 간의 관계를 정밀하게 분석하고, 이탈 정도에 따라 시각적인 경고를 제공함으로써 사용자가 경로를 유지하고 안전하게 러닝을 즐길 수 있도록 지원합니다.
Highlights
- 경로 이탈 감지 및 알림 기능 추가: 사용자의 현재 위치가 미리 정의된 코스에서 벗어났는지 실시간으로 감지하고, 이탈 정도에 따라 시각적인 경고를 제공하는
useCourseValidation훅과 관련 UI가 추가되었습니다. - 지리 공간 계산 라이브러리 도입: 정확한 위치 기반 계산을 위해
@turf/turf라이브러리를 도입하여 코스 내 위치 여부, 코스 경계까지의 거리 등을 효율적으로 계산합니다. - 위치 추적 및 권한 처리 로직 개선:
useInitialLocation훅을 통해 초기 위치 로딩 및 권한 요청 로직을 개선하고,useLocationTracking에서 이를 활용하도록 리팩토링하여 안정성을 높였습니다. - 중앙 집중식 상태 관리:
useRunStore에 경로 검증 관련 상태(isOnCourse,courseDeviation,validationHistory) 및 액션을 추가하여 애플리케이션 전반에서 경로 이탈 상태를 일관되게 관리할 수 있도록 했습니다. - 런 스크린 UI/UX 개선: 경로 이탈 시 심각도에 따른 오버레이 알림을 표시하고, 현재 위치로 지도를 이동시키는 버튼을 추가하는 등 런 스크린의 사용자 경험을 향상시켰습니다.
Using Gemini Code Assist
The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.
Invoking Gemini
You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.
| Feature | Command | Description |
|---|---|---|
| Code Review | /gemini review |
Performs a code review for the current pull request in its current state. |
| Pull Request Summary | /gemini summary |
Provides a summary of the current pull request in its current state. |
| Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments. |
| Help | /gemini help |
Displays a list of available commands. |
Customization
To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.
Limitations & Feedback
Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.
You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.
Footnotes
-
Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩
There was a problem hiding this comment.
Code Review
경로 이탈 알림 기능 추가를 위한 PR 잘 보았습니다. 전반적으로 새로운 훅과 유틸리티 함수를 도입하여 기능적으로 잘 분리되어 있고, 상태 관리 로직도 적절하게 확장되었습니다. 다만, 몇 가지 개선할 점이 보입니다. 스타일 가이드에 따라 'any' 타입 사용을 지양하고, 매직 넘버를 상수로 대체하는 것이 좋겠습니다. 또한, 중복된 로직을 통합하고, courseValidation.ts 유틸리티의 일부 로직에서 발견된 잠재적인 버그와 타입 문제를 수정해야 합니다. 특히 calculateSafetyDistance 함수는 현재 데이터 구조와 맞지 않아 오류를 발생시킬 가능성이 높으므로 반드시 확인이 필요합니다. 자세한 내용은 각 파일에 남긴 코멘트를 참고해주세요.
| export function calculateSafetyDistance( | ||
| currentLocation: Position, | ||
| topology: CourseTopologyResponse, | ||
| ): number { |
There was a problem hiding this comment.
| severity: !result.isOnCourse | ||
| ? result.distanceFromCourse && result.distanceFromCourse > 100 | ||
| ? 'high' | ||
| : result.distanceFromCourse && result.distanceFromCourse > 50 | ||
| ? 'medium' | ||
| : 'low' | ||
| : 'low', |
There was a problem hiding this comment.
| if (validationResult.distanceFromCourse > 100) { | ||
| deviationSeverity = 'high'; | ||
| } else if (validationResult.distanceFromCourse > 50) { | ||
| deviationSeverity = 'medium'; | ||
| } else { | ||
| deviationSeverity = 'low'; | ||
| } |
| function validateLocationWithLines( | ||
| currentLocation: Position, | ||
| topology: CourseTopologyResponse, | ||
| options: CourseValidationOptions = {}, | ||
| ): CourseValidationResult { |
There was a problem hiding this comment.
| const timer = setTimeout(() => { | ||
| refreshLocation(); | ||
| }, 2000); // 2초 후에 새로고침 시도 |
| */ | ||
| export function convertTopologyToMultiPolygon( | ||
| topology: CourseTopologyResponse, | ||
| ): any { |
| console.log('🔍 코스 검증 결과:', { | ||
| currentLocation: [ | ||
| currentLocation[0].toFixed(6), | ||
| currentLocation[1].toFixed(6), | ||
| ], | ||
| isInside, | ||
| distanceFromCourse: distanceFromCourse.toFixed(2) + 'm', | ||
| tolerance: tolerance + 'm', | ||
| finalResult: isOnCourse ? '경로 내부' : '경로 외부', | ||
| }); |
| console.log('📍 [LocationTracking] 위치 업데이트:', { | ||
| latitude, | ||
| longitude, | ||
| timestamp: new Date().toISOString(), | ||
| }); |
| recentValidations.length >= 3 && | ||
| recentValidations.every((v) => v.isOnCourse) |
| console.log('🔍 [CourseValidation] 검증 실행:', { | ||
| currentPosition, | ||
| isTracking, | ||
| routeCoordinatesLength: routeCoordinates.length, | ||
| isDeviating: deviationResult.isDeviating, | ||
| severity: deviationResult.deviationSeverity, | ||
| distance: deviationResult.validationResult.distanceFromCourse, | ||
| }); |
There was a problem hiding this comment.
Code Review
이번 PR은 경로 이탈 알림 기능을 구현한 것으로, useCourseValidation 훅과 관련 유틸리티 함수들이 추가되었습니다. 전반적으로 기능 구현은 잘 이루어졌으나, 몇 가지 개선점을 제안합니다.
주요 피드백은 다음과 같습니다:
useInitialLocation훅의 권한 처리 로직에 있는 버그 수정courseValidation.ts유틸리티의 심각한 타입 불일치 문제 해결- 코드 전반에 하드코딩된 매직 넘버와 스타일 값을 상수로 추출하여 스타일 가이드 준수
- 디버깅용
console.log제거
이러한 부분들을 개선하면 코드의 안정성과 유지보수성이 크게 향상될 것입니다.
| export function validateLocationWithTopology( | ||
| currentLocation: Position, | ||
| topology: CourseTopologyResponse, | ||
| options: CourseValidationOptions = {}, | ||
| ): CourseValidationResult { | ||
| // 🔍 라인 데이터를 그대로 사용하여 검증 | ||
| return validateLocationWithLines(currentLocation, topology, options); |
There was a problem hiding this comment.
CourseTopologyResponse의 shape 타입([number, number][][])과 실제 사용 방식 간에 심각한 불일치가 있습니다.
validateLocationWithLines 함수에서는 shape를 Position[][]으로 올바르게 처리하는 반면, 동일 파일 내의 calculateSafetyDistance 및 convertTopologyToMultiPolygon 함수에서는 Position[][][]으로 잘못 처리하고 있습니다. 이 불일치는 런타임 에러를 유발할 수 있습니다.
shape의 정확한 데이터 구조를 확인하고, 모든 관련 함수에서 일관된 방식으로 타입을 처리하도록 수정해야 합니다. 예를 들어, calculateSafetyDistance의 topology.shape.map(...) 부분은 validateLocationWithLines의 순회 방식과 일치해야 합니다.
| const deviationStartTime = new Date( | ||
| lastValidationTime.getTime() - | ||
| (validationHistory.length - deviationStartIndex) * validationInterval, | ||
| ); | ||
|
|
||
| return now.getTime() - deviationStartTime.getTime(); |
There was a problem hiding this comment.
| if (currentStatus !== 'granted' && requestPermission) { | ||
| const { status } = await Location.requestForegroundPermissionsAsync(); | ||
| if (status !== 'granted') { | ||
| setError('위치 접근 권한이 거절되었습니다.'); | ||
| setLoading(false); | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| // 권한이 없으면 위치 가져오기 시도하지 않음 | ||
| if (currentStatus !== 'granted') { | ||
| setError('위치 접근 권한이 필요합니다.'); | ||
| setLoading(false); | ||
| return; | ||
| } |
There was a problem hiding this comment.
권한 요청 로직에 버그가 있습니다. 23-30행에서 권한을 요청하고 사용자가 승인하더라도, currentStatus 변수가 갱신되지 않아 33행의 조건문에 걸려 함수가 조기 종료될 수 있습니다. 이로 인해 권한이 있어도 위치 정보를 가져오지 못하는 문제가 발생할 수 있습니다.
아래와 같이 수정하여 status 변수를 재할당하고 로직을 간소화하는 것을 권장합니다.
(async () => {
let { status } = await Location.getForegroundPermissionsAsync();
if (status !== 'granted' && requestPermission) {
const permissionResult = await Location.requestForegroundPermissionsAsync();
status = permissionResult.status;
}
if (status !== 'granted') {
setError('위치 접근 권한이 필요합니다.');
setLoading(false);
return;
}
// ... 위치 정보 가져오기 로직
})(); let { status } = await Location.getForegroundPermissionsAsync();
// 권한이 없고 요청이 필요한 경우에만 권한 요청
if (status !== 'granted' && requestPermission) {
const permissionResult = await Location.requestForegroundPermissionsAsync();
status = permissionResult.status;
}
// 권한이 없으면 위치 가져오기 시도하지 않음
if (status !== 'granted') {
setError('위치 접근 권한이 필요합니다.');
setLoading(false);
return;
}| console.log('🔍 [CourseValidation] 검증 실행:', { | ||
| currentPosition, | ||
| isTracking, | ||
| routeCoordinatesLength: routeCoordinates.length, | ||
| isDeviating: deviationResult.isDeviating, | ||
| severity: deviationResult.deviationSeverity, | ||
| distance: deviationResult.validationResult.distanceFromCourse, | ||
| }); |
| console.log('📍 [LocationTracking] 위치 업데이트:', { | ||
| latitude, | ||
| longitude, | ||
| timestamp: new Date().toISOString(), | ||
| }); |
| <View | ||
| style={{ | ||
| width: 16, | ||
| height: 16, | ||
| borderRadius: 8, | ||
| backgroundColor: theme.colors.secondary[500], | ||
| borderWidth: 3, | ||
| borderColor: '#ffffff', | ||
| shadowColor: '#000', | ||
| shadowOffset: { width: 0, height: 2 }, | ||
| shadowOpacity: 0.3, | ||
| shadowRadius: 4, | ||
| elevation: 5, | ||
| }} | ||
| /> |
There was a problem hiding this comment.
현재 위치 마커에 인라인 스타일이 사용되었습니다. 스타일 가이드에 따라 인라인 스타일은 지양하고 StyleSheet.create를 사용해야 합니다.1 또한, 색상('#ffffff', '#000')과 크기(16, 8, 3 등) 값들이 하드코딩되어 있습니다. 이 값들은 테마 또는 상수 파일에서 관리하는 것이 좋습니다.2
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
currentLocationMarker: {
width: 16,
height: 16,
borderRadius: 8,
backgroundColor: theme.colors.secondary[500],
borderWidth: 3,
borderColor: theme.colors.white, // theme.colors.white 등으로 변경
shadowColor: theme.colors.black, // theme.colors.black 등으로 변경
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 5,
}
});
// ...
<View style={styles.currentLocationMarker} />Style Guide References
Footnotes
| tolerance: 5, // 5미터 허용 오차 (매우 엄격하게) | ||
| enableDistanceCalculation: true, | ||
| }, | ||
| enableRealTimeValidation: true, // 실시간 검증 활성화 | ||
| validationInterval: 1000, // 1초마다 검증 |
| severity: !result.isOnCourse | ||
| ? result.distanceFromCourse && result.distanceFromCourse > 100 | ||
| ? 'high' | ||
| : result.distanceFromCourse && result.distanceFromCourse > 50 | ||
| ? 'medium' | ||
| : 'low' | ||
| : 'low', |
There was a problem hiding this comment.
경로 이탈 심각도를 결정하는 거리 임계값(100, 50)이 하드코딩되어 있습니다. 이러한 매직 넘버는 가독성을 해치고 유지보수를 어렵게 만듭니다. constants.ts와 같은 파일에 의미 있는 이름의 상수로 정의하여 사용하는 것이 좋습니다.1
// constants.ts
export const DEVIATION_SEVERITY_HIGH_THRESHOLD_M = 100;
export const DEVIATION_SEVERITY_MEDIUM_THRESHOLD_M = 50;
// run.ts
severity: !result.isOnCourse
? result.distanceFromCourse && result.distanceFromCourse > DEVIATION_SEVERITY_HIGH_THRESHOLD_M
? 'high'
: result.distanceFromCourse && result.distanceFromCourse > DEVIATION_SEVERITY_MEDIUM_THRESHOLD_M
? 'medium'
: 'low'
: 'low',Style Guide References
Footnotes
-
매직 넘버(예: 200, 0.5 등)는 이름 있는 상수로 치환해야 합니다. ↩
작업 내용
참고 사항