Feature: UI 전면 개선#47
Conversation
- Card, FloatingButton, Modal, TabNavigation, Detail, Run, DrawMap 등 여러 컴포넌트의 색상 변경 - LinearGradient를 사용하여 버튼 및 텍스트 배경 스타일 추가 - 색상 일관성을 위해 테마 색상 조정
- LinearGradient 제거 및 ButtonContainer에 배경색 '#ff6b35' 추가 - 불필요한 코드 및 주석 정리
- navigation prop의 타입을 'Run'에서 'RunTab'으로 변경하여 정확한 네비게이션 경로 반영
- Card 컴포넌트에 variant prop 추가하여 스타일 조정 - useRouteApi에 주변 경로 검색 기능 추가 - RunTabNavigator에서 추천 경로 로드 및 표시 기능 구현 - RecommendationContainer 컴포넌트 추가하여 추천 경로 리스트 표시 - useLongPress 훅에서 불필요한 상태 제거 및 코드 정리 - Home 페이지에서 추천 경로 관련 코드 정리
- RecommendationContainer 컴포넌트에서 departure 좌표를 배열 형식으로 변경하여 reverseGeocode 함수 호출 시 올바른 인자 전달 - courses.types.ts에서 departure 타입을 RouteCoordinate에서 [number, number]로 수정
- useLongPress 훅에서 pressProgress 상태를 제거하여 코드 간결성 향상
- 코스 선택 시 최소 이동 거리 체크 로직 추가하여 유효성 검사 강화 - 런닝 기록 저장 요청 및 응답에 대한 로그 추가로 디버깅 용이성 향상 - API 응답 상태코드 및 데이터 로그 추가하여 오류 처리 개선
- RunTabNavigator에서 StartButtonContainer의 위치를 아래로 20px 조정 - TabNavigator에서 탭 바 높이를 8px 증가시키고 배경색을 반투명으로 변경 - TabNavigator의 스타일에 패딩 및 테두리 추가 - Route 페이지의 FloatingButton에 기본 스타일 적용 - RecommendationContainer에서 카드의 서브타이틀 형식 수정
- RunTabNavigator의 StartButtonProgress와 Detail 페이지의 DrawButtonProgress 배경색을 '#4ECDC4'에서 '#000000'으로 변경하여 UI 일관성 향상
- useRunningRecords 훅을 추가하여 런닝 기록을 로드하고 관리하는 기능 구현 - useRunningDashboard 훅을 추가하여 런닝 대시보드를 로드하는 기능 구현 - Records 페이지에 시간 범위 선택기, 통계 표시 및 기록 목록 컴포넌트 추가 - RecordsList 및 StatsDisplay 컴포넌트 생성하여 UI 구성 - 시간 범위 계산 및 표시를 위한 유틸리티 함수 추가 - API 호출을 위한 서비스 함수(searchRunningRecords, getRunningDashboard) 추가 - 관련 타입 정의(records.types.ts) 추가하여 타입 안정성 향상
- RecommendationContainer에 안내 카드 추가하여 사용자에게 런닝 시작을 유도 - FlatList에서 데이터 배열을 안내 카드와 추천 경로로 구성하여 표시 방식 개선 - Card 컴포넌트의 스타일을 조정하여 UI 일관성 향상 - GuideCard 및 관련 텍스트 스타일 추가하여 안내 카드의 가독성 및 디자인 개선
- CardContainer의 flex 속성을 수정하여 UI 일관성 향상 - CardImage의 크기를 조정하여 이미지 비율 개선 - GuideCard의 최소 높이를 조정하여 디자인 최적화
- RunTabNavigator에서 '바로가기'를 '바로 달리기'로 변경하여 명확한 의미 전달 - Detail 페이지에서 버튼 텍스트를 '그리기...'에서 '달리기...'로 수정하여 사용자 경험 개선
- Map 컴포넌트에 onMapReady 프로퍼티 추가하여 지도 로딩 완료 시 콜백 실행 - DrawMap 컴포넌트에서 onMapReady를 사용하여 지도 준비 상태 관리 - useMapGestures 훅에서 불필요한 상태 초기화 코드 제거 - Draw 페이지에서 탭 이탈 시 그린 선 초기화 로직 추가 - 스타일 개선: 그린 선 및 완료된 선의 색상 변경
- LoadingIndicator 컴포넌트에 메시지 프로퍼티를 추가하여 로딩 메시지를 동적으로 설정 가능하도록 개선 - Draw 페이지에서 로딩 중 메시지를 "매칭중..."으로 변경 - RouteSave 페이지에서 내비게이션을 'TabNavigator'의 'Home' 화면으로 수정하여 사용자 흐름 개선
- Draw 탭 이탈 시 그린 선 초기화 로직을 RouteSave에서 돌아오는 경우를 제외하도록 수정 - RunTabNavigator를 리셋하는 RunTabWithReset 컴포넌트 추가하여 사용자 흐름 개선 - Route 페이지의 버튼 배경색을 '#111827'에서 '#000000'으로 변경하여 UI 일관성 향상
- useMapCapture 훅에 캡처 전후 대기 시간을 추가하여 안정성 향상 - DrawMap 컴포넌트에서 캡처 상태에 따라 그리기 소스 표시 로직을 개선하여 사용자 경험 향상 - Draw 페이지 이탈 시 그린 선 초기화 로직을 간소화하여 코드 가독성 개선
- eas.json 파일에 Android 빌드 타입을 "apk"로 설정하여 Android 빌드 구성 개선
- eas.json 파일에 Android 빌드 타입을 "apk"로 설정하여 Android 빌드 구성 개선 - 불필요한 공백을 제거하여 코드 가독성 향상
- Google Sign-In 초기화 과정에서 환경변수 상태 및 설정 정보를 로그로 출력하여 디버깅 용이성 향상 - 로그인 시도 및 토큰 요청 과정에서 상세한 로그를 추가하여 문제 발생 시 원인 파악 용이 - 서버 로그인 요청 시 오류 상태에 따른 상세한 로그를 추가하여 사용자에게 가능한 원인 안내 - 토큰 갱신 및 로그아웃 과정에서도 로그를 추가하여 전체적인 흐름 추적 가능 - 로그인 상태 확인 시 현재 사용자 정보를 로그로 출력하여 상태 확인 용이성 향상
- Google Sign-In 초기화 및 로그인 과정에서 로그 메시지를 영어로 변경하여 일관성 유지 - 오류 메시지를 개선하여 사용자에게 더 명확한 원인 안내 제공 - 전체적인 로그 메시지의 가독성을 향상시켜 디버깅 용이성 증대
- useRouteData, useBookmarkedCourses, useCompletedCourses 훅에서 로딩 상태 확인 방식을 간소화하여 가독성 향상 - 불필요한 로그 메시지를 제거하고, API 호출 시 요청 및 응답 로그를 삭제하여 코드 정리 - Route 컴포넌트의 useEffect 의존성에서 불필요한 항목을 제거하여 성능 개선
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
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!
이 Pull Request는 애플리케이션의 사용자 인터페이스를 대대적으로 개편하고 핵심 기능을 재구성합니다. 주요 변경 사항으로는 런닝 시작 경험을 간소화하기 위해 'Route' 탭을 'Run' 탭으로 통합하고, 사용자의 런닝 데이터를 시각적으로 제공하는 'Records' 탭을 새로 도입한 것입니다. 또한, 주변 코스 추천과 같은 새로운 기능이 추가되었으며, 전반적인 디자인 요소와 디버깅 편의성이 향상되었습니다.
Highlights
- UI 전면 개편 및 탭 구조 변경: 기존 'Route' 탭이 사라지고, 'Run' 탭 내에 '바로 달리기'와 '코스 선택하기' 기능이 통합되었습니다. 이전 'Route' 탭 자리에는 새로운 'Records' 탭이 추가되어 달리기 이력과 통계를 확인할 수 있습니다.
- 런닝 기록 및 통계 기능 추가: 사용자의 런닝 기록을 조회하고, 주/월/년/전체 기간별 통계를 확인할 수 있는 'Records' 페이지와 관련 API 훅이 새로 추가되었습니다.
- 주변 코스 추천 기능 도입: 새로운 'Run' 탭의 '바로 달리기' 화면에서 현재 위치 기반으로 주변 코스를 추천해주는 기능이 추가되었습니다.
- Google 로그인 및 위치 추적 로깅 강화: Google 로그인 과정과 위치 추적 로직에 대한 상세한 콘솔 로깅이 추가되어 디버깅 및 문제 해결에 용이해졌습니다.
- 디자인 시스템 및 컴포넌트 개선: Card 컴포넌트에
variantprop이 추가되어 다양한 디자인을 지원하며, 전반적인 색상 팔레트가 업데이트되고, 버튼에 LinearGradient가 적용되는 등 UI 컴포넌트의 시각적 개선이 이루어졌습니다.
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은 앱의 UI를 전면적으로 개선하고 네비게이션 구조를 개편하는 대규모 작업이네요. 기존 'Route' 탭을 'Run' 탭에 통합하고, 달리기 기록과 통계를 볼 수 있는 'Records' 탭을 새로 추가하는 등 사용자 경험을 크게 향상시키는 변경 사항들이 포함되어 있습니다. 전체적으로 코드 구조가 더 명확해지고 새로운 기능들이 잘 구현된 것 같습니다.
다만, 코드 전반에 걸쳐 스타일 가이드에 위배되는 부분들이 다수 발견되었습니다. 특히 색상 값이나 특정 수치들이 상수로 관리되지 않고 하드코딩되어 있어 유지보수성을 저해할 수 있습니다. 또한, 디버깅 목적으로 추가된 것으로 보이는 console.log 문들이 여러 파일에 남아있어 병합 전 정리가 필요해 보입니다. 몇 가지 구체적인 개선 사항들을 아래 코멘트로 남겼으니 확인 부탁드립니다.
| // 캡처 전에 잠시 대기하여 현재 렌더링이 완료되도록 함 | ||
| await new Promise((resolve) => setTimeout(resolve, 100)); | ||
|
|
||
| setIsCapturing(true); | ||
|
|
||
| // 소스 제거 후 추가 대기 | ||
| await new Promise((resolve) => setTimeout(resolve, 200)); |
| try { | ||
| console.log('🔧 Google Sign-In initialization started'); | ||
|
|
||
| const webClientId = process.env.EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID; | ||
| const iosClientId = process.env.EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID; | ||
|
|
||
| // CRITICAL: Always log the Web Client ID | ||
| console.error('EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID:', webClientId); | ||
| console.error('EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID:', iosClientId); | ||
|
|
||
| console.log('Web Client ID status:', webClientId ? 'SET' : '❌ NOT SET'); | ||
| console.log('iOS Client ID status:', iosClientId ? 'SET' : '❌ NOT SET'); | ||
|
|
||
| if (webClientId) { | ||
| console.log('Web Client ID length:', webClientId.length); | ||
| console.log( | ||
| 'Web Client ID first 20 chars:', | ||
| webClientId.substring(0, 20) + '...', | ||
| ); | ||
| } | ||
|
|
||
| if (iosClientId) { | ||
| console.log('iOS Client ID length:', iosClientId.length); | ||
| console.log( | ||
| 'iOS Client ID first 20 chars:', | ||
| iosClientId.substring(0, 20) + '...', | ||
| ); | ||
| } | ||
|
|
||
| if (!webClientId) { | ||
| console.error('❌ Web Client ID is NOT SET!'); | ||
| console.error( | ||
| 'Check EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID environment variable', | ||
| ); | ||
| } | ||
|
|
||
| if (!iosClientId) { | ||
| console.error('❌ iOS Client ID is NOT SET!'); | ||
| console.error( | ||
| 'Check EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID environment variable', | ||
| ); | ||
| } | ||
|
|
||
| const config = { | ||
| webClientId, | ||
| iosClientId, | ||
| profileImageSize: 120, | ||
| }; | ||
|
|
||
| console.log('Google Sign-In config:', config); | ||
| GoogleSignin.configure(config); | ||
|
|
||
| console.log('✅ Google Sign-In initialization completed'); | ||
| } catch (error) { | ||
| console.error('❌ Google Sign-In initialization error:', error); | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| export async function signInWithGoogle(): Promise<GoogleSignInResponse> { | ||
| try { | ||
| console.log('Google login started'); | ||
|
|
||
| console.log('Checking Play Services...'); | ||
| await GoogleSignin.hasPlayServices(); | ||
| await GoogleSignin.signIn(); | ||
| const { idToken } = await GoogleSignin.getTokens(); | ||
| console.log('Play Services check completed'); | ||
|
|
||
| console.log('Attempting Google login...'); | ||
| const signInResult = await GoogleSignin.signIn(); | ||
| console.log('Google login successful:', signInResult); | ||
|
|
||
| console.log('Getting tokens...'); | ||
| const tokens = await GoogleSignin.getTokens(); | ||
| console.log('Full token response:', tokens); | ||
|
|
||
| const { idToken, accessToken } = tokens; | ||
| console.log('ID Token status:', idToken ? 'ISSUED' : 'NOT ISSUED'); | ||
| console.log('Access Token status:', accessToken ? 'ISSUED' : 'NOT ISSUED'); | ||
|
|
||
| if (idToken) { | ||
| console.log('ID Token length:', idToken.length); | ||
| console.log('ID Token first 20 chars:', idToken.substring(0, 20) + '...'); | ||
| } | ||
|
|
||
| if (!idToken) { | ||
| throw new Error('구글 로그인 중 ID 토큰을 받지 못했습니다.'); | ||
| console.error('❌ ID Token was NOT issued'); | ||
| console.error('Possible causes:'); | ||
| console.error('1. Google Console OAuth client ID configuration error'); | ||
| console.error('2. Production bundle ID not registered in Google Console'); | ||
| console.error('3. Redirect URI configuration error'); | ||
| console.error('4. Wrong client ID in .env file'); | ||
| console.error('5. OAuth consent screen not published to production'); | ||
| throw new Error('Failed to receive ID token during Google login.'); | ||
| } | ||
|
|
||
| const { data } = await api.post<GoogleSignInResponse>('/auth/google', { | ||
| idToken, | ||
| console.log('Sending login request to server...'); | ||
| console.log('Request URL:', '/auth/google'); | ||
| console.log('Request payload:', { | ||
| idToken: idToken.substring(0, 20) + '...', | ||
| }); | ||
|
|
||
| let data: GoogleSignInResponse; | ||
| try { | ||
| const response = await api.post<GoogleSignInResponse>('/auth/google', { | ||
| idToken, | ||
| }); | ||
| data = response.data; | ||
| console.log('✅ Server login successful:', data); | ||
| } catch (serverError: any) { | ||
| console.error('❌ Server login failed:', serverError); | ||
| console.error('Server response status:', serverError.response?.status); | ||
| console.error('Server response data:', serverError.response?.data); | ||
| console.error('Server response headers:', serverError.response?.headers); | ||
|
|
||
| if (serverError.response?.status === 400) { | ||
| console.error('400 Error - Possible causes:'); | ||
| console.error('1. Invalid ID token format'); | ||
| console.error('2. Google Console OAuth configuration error'); | ||
| console.error('3. Client ID mismatch between server and app'); | ||
| console.error('4. Wrong OAuth consent screen configuration'); | ||
| } else if (serverError.response?.status === 401) { | ||
| console.error('401 Error - Possible causes:'); | ||
| console.error('1. ID token expired'); | ||
| console.error('2. Google token verification failed on server'); | ||
| console.error('3. Server Google OAuth configuration error'); | ||
| console.error('4. Invalid client ID or secret on server'); | ||
| } else if (serverError.response?.status === 500) { | ||
| console.error('500 Error - Server internal error'); | ||
| console.error('Check server logs for details'); | ||
| } | ||
|
|
||
| throw serverError; | ||
| } | ||
|
|
||
| return data; | ||
| } catch (error) { | ||
| console.error('Google login overall error:', error); | ||
|
|
||
| if (isErrorWithCode(error)) { | ||
| console.error('Google login code error:', error.code); | ||
| switch (error.code) { | ||
| case statusCodes.SIGN_IN_CANCELLED: | ||
| throw new Error('사용자가 로그인을 취소했습니다.'); | ||
| console.error('User cancelled login'); | ||
| throw new Error('User cancelled login.'); | ||
| case statusCodes.IN_PROGRESS: | ||
| throw new Error('로그인이 진행 중입니다.'); | ||
| console.error('Login in progress'); | ||
| throw new Error('Login in progress.'); | ||
| case statusCodes.PLAY_SERVICES_NOT_AVAILABLE: | ||
| console.error('Play Services not available'); | ||
| throw new Error( | ||
| '구글 플레이 서비스를 사용할 수 없거나 버전이 오래되었습니다.', | ||
| 'Google Play Services is not available or version is outdated.', | ||
| ); | ||
| default: | ||
| throw new Error('구글 로그인 중 오류가 발생했습니다.'); | ||
| console.error('Unknown Google login error:', error.code); | ||
| throw new Error('Error occurred during Google login.'); | ||
| } | ||
| } | ||
| throw new Error('로그인 중 오류가 발생했습니다. 다시 시도해주세요.'); | ||
|
|
||
| console.error('General login error:', error); | ||
| throw new Error('Error occurred during login. Please try again.'); | ||
| } | ||
| } | ||
|
|
||
| export async function refreshToken(): Promise<RefreshTokenResponse> { | ||
| try { | ||
| console.log('Token refresh started'); | ||
| const { data } = await api.post<RefreshTokenResponse>('/auth/refresh'); | ||
| console.log('Token refresh response:', data); | ||
|
|
||
| if (!data.accessToken) { | ||
| throw new Error('토큰 갱신에 실패했습니다.'); | ||
| console.error('Token refresh failed: accessToken is missing'); | ||
| throw new Error('Token refresh failed.'); | ||
| } | ||
|
|
||
| console.log('Token refresh successful'); | ||
| return data; | ||
| } catch (error) { | ||
| throw new Error('토큰 갱신 중 오류가 발생했습니다.'); | ||
| console.error('Token refresh error:', error); | ||
| throw new Error('Error occurred during token refresh.'); | ||
| } | ||
| } | ||
|
|
||
| export async function signOut(): Promise<void> { | ||
| try { | ||
| console.log('Google logout started'); | ||
| await GoogleSignin.signOut(); | ||
| console.log('Google logout successful'); | ||
| } catch (error) { | ||
| console.error('Google 로그아웃 오류:', error); | ||
| console.error('Google logout error:', error); | ||
| } | ||
| } | ||
|
|
||
| export async function isSignedIn(): Promise<boolean> { | ||
| try { | ||
| console.log('Checking login status...'); | ||
| const userInfo = await GoogleSignin.getCurrentUser(); | ||
| console.log('Current user info:', userInfo ? 'LOGGED IN' : 'NOT LOGGED IN'); | ||
| return userInfo !== null; | ||
| } catch (error) { | ||
| console.error('로그인 상태 확인 오류:', error); | ||
| console.error('Login status check error:', error); | ||
| return false; | ||
| } | ||
| } |
| console.log('📊 [useRunningRecords] loadRecords 호출:', { | ||
| params, | ||
| reset, | ||
| loading, | ||
| }); | ||
|
|
||
| if (loading) { | ||
| console.log('📊 [useRunningRecords] 이미 로딩 중이므로 스킵'); | ||
| return; | ||
| } | ||
|
|
||
| setLoading(true); | ||
| setError(null); | ||
|
|
||
| try { | ||
| const requestParams = { | ||
| ...params, | ||
| limit: 20, | ||
| }; | ||
|
|
||
| // reset이 아닌 경우에만 cursor 추가 | ||
| if (!reset && params.cursor) { | ||
| requestParams.cursor = params.cursor; | ||
| } | ||
|
|
||
| console.log('📊 [useRunningRecords] API 요청 파라미터:', requestParams); | ||
| const response = await searchRunningRecords(requestParams, accessToken); | ||
|
|
||
| console.log('📊 [useRunningRecords] API 응답:', response); | ||
|
|
||
| if (reset) { | ||
| setRecords(response.results); | ||
| } else { | ||
| setRecords((prev) => [...prev, ...response.results]); | ||
| } | ||
|
|
||
| // nextCursor가 현재 데이터의 마지막 ID와 같으면 더 이상 데이터가 없음 | ||
| const lastResultId = response.results[response.results.length - 1]?.id; | ||
| const hasMoreData = | ||
| response.nextCursor && response.nextCursor.id !== lastResultId; | ||
|
|
||
| console.log('📊 [useRunningRecords] 데이터 종료 조건 확인:', { | ||
| lastResultId, | ||
| nextCursor: response.nextCursor, | ||
| hasMoreData, | ||
| resultsLength: response.results.length, | ||
| }); | ||
|
|
||
| if (hasMoreData) { | ||
| setCursor(response.nextCursor); | ||
| setHasMore(true); | ||
| } else { | ||
| setCursor(null); | ||
| setHasMore(false); | ||
| } | ||
| } catch (error: unknown) { | ||
| console.error('📊 [useRunningRecords] API 오류:', error); | ||
| let errorMessage = '런닝 기록을 불러오는데 실패했습니다.'; | ||
|
|
||
| if (error && typeof error === 'object' && 'response' in error) { | ||
| const axiosError = error as AxiosErrorResponse; | ||
| const status = axiosError.status; | ||
|
|
||
| if (status === 500) { | ||
| errorMessage = | ||
| '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'; | ||
| } else if (status === 401) { | ||
| errorMessage = '로그인이 필요합니다.'; | ||
| } else if (status === 403) { | ||
| errorMessage = '접근 권한이 없습니다.'; | ||
| } | ||
| } else if (error instanceof Error) { | ||
| errorMessage = error.message; | ||
| } | ||
|
|
||
| setError(errorMessage); | ||
| } finally { | ||
| setLoading(false); | ||
| } |
| const Tab = createBottomTabNavigator<TabParamList>(); | ||
|
|
||
| const TAB_BAR_HEIGHT = 60; | ||
| const TAB_BAR_HEIGHT = 68; |
| @@ -0,0 +1,294 @@ | |||
| import { useCallback, useState, useEffect } from 'react'; | |||
| if (finalMode === 'image-with-text') { | ||
| const contentWidth = screenWidth - 64; | ||
| const imageSize = contentWidth * 0.45; | ||
| const imageSize = contentWidth * 0.36; // 0.45에서 0.36으로 변경 (약 20% 감소) |
There was a problem hiding this comment.
스타일 가이드에서는 매직 넘버 사용을 지양하고 이름 있는 상수로 치환할 것을 권장합니다.1 0.36이라는 값을 IMAGE_SIZE_RATIO와 같은 명확한 이름의 상수로 정의하여 사용하면 코드의 가독성과 유지보수성을 높일 수 있습니다.
const IMAGE_SIZE_RATIO = 0.36;
const imageSize = contentWidth * IMAGE_SIZE_RATIO; // 0.45에서 0.36으로 변경 (약 20% 감소)
Style Guide References
Footnotes
-
매직 넘버(예: 200, 0.5 등)는 이름 있는 상수로 치환해야 합니다. ↩
| throw new Error('User cancelled login.'); | ||
| case statusCodes.IN_PROGRESS: | ||
| throw new Error('로그인이 진행 중입니다.'); | ||
| console.error('Login in progress'); | ||
| throw new Error('Login in progress.'); | ||
| case statusCodes.PLAY_SERVICES_NOT_AVAILABLE: | ||
| console.error('Play Services not available'); | ||
| throw new Error( | ||
| '구글 플레이 서비스를 사용할 수 없거나 버전이 오래되었습니다.', | ||
| 'Google Play Services is not available or version is outdated.', | ||
| ); | ||
| default: | ||
| throw new Error('구글 로그인 중 오류가 발생했습니다.'); | ||
| console.error('Unknown Google login error:', error.code); | ||
| throw new Error('Error occurred during Google login.'); |
| console.log('📤 [RunningService] 런닝 기록 저장 API 요청'); | ||
| console.log('📤 [RunningService] 엔드포인트:', endpoint); | ||
| console.log('📤 [RunningService] 쿼리 파라미터:', params); | ||
| console.log('📤 [RunningService] 요청 페이로드:', { | ||
| path: `${requestData.path.length}개 좌표`, | ||
| startAt: new Date(requestData.startAt).toISOString(), | ||
| endAt: new Date(requestData.endAt).toISOString(), | ||
| pace: requestData.pace, | ||
| calories: requestData.calories, | ||
| imageUrl: requestData.imageUrl ? '있음' : '없음', | ||
| }); | ||
|
|
||
| const response = await api.post('/api/running/records', requestData, { | ||
| const response = await api.post(endpoint, requestData, { | ||
| headers: { | ||
| Authorization: `Bearer ${accessToken}`, | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| params, | ||
| }); | ||
|
|
||
| console.log('📥 [RunningService] 런닝 기록 저장 API 응답'); | ||
| console.log('📥 [RunningService] 응답 상태:', response.status); | ||
| console.log('📥 [RunningService] 응답 데이터:', response.data); | ||
|
|
||
| return response.data; | ||
| } catch (error: unknown) { | ||
| console.error('❌ [RunningService] 런닝 기록 저장 API 오류:', error); | ||
| throw error; |
| resetRunState: () => { | ||
| set((state) => ({ | ||
| ...initialUIState, | ||
| ...initialErrorState, | ||
| ...initialRunningState, | ||
| // 코스 데이터는 유지 (currentCourseId, currentCourseData 보존) | ||
| courseTopology: null, | ||
| ...initialLocationTrackingState, | ||
| ...initialCourseValidationState, | ||
| })); | ||
| set((state) => { | ||
| // 런닝이 진행 중이면 상태를 보존 | ||
| if (state.startTime) { | ||
| console.log('🔄 [RunStore] 런닝 상태 보존 - 통계 초기화되지 않음'); | ||
| return { | ||
| ...initialUIState, | ||
| ...initialErrorState, | ||
| // 런닝 상태는 보존 | ||
| startTime: state.startTime, | ||
| pausedTime: state.pausedTime, | ||
| pauseStartTime: state.pauseStartTime, | ||
| stats: state.stats, | ||
| // 코스 데이터는 유지 (currentCourseId, currentCourseData 보존) | ||
| courseTopology: null, | ||
| // 위치 추적 상태는 보존 (경로 데이터 유지) | ||
| routeCoordinates: state.routeCoordinates, | ||
| location: state.location, | ||
| isTracking: state.isTracking, | ||
| subscriber: state.subscriber, | ||
| errorMsg: state.errorMsg, | ||
| ...initialCourseValidationState, | ||
| }; | ||
| } | ||
|
|
||
| // 런닝이 시작되지 않은 경우에만 완전 초기화 | ||
| console.log('🔄 [RunStore] 런닝 상태 완전 초기화 - 통계 초기화됨'); | ||
| return { | ||
| ...initialUIState, | ||
| ...initialErrorState, | ||
| ...initialRunningState, | ||
| // 코스 데이터는 유지 (currentCourseId, currentCourseData 보존) | ||
| courseTopology: null, | ||
| ...initialLocationTrackingState, | ||
| ...initialCourseValidationState, | ||
| }; | ||
| }); | ||
| }, |
| const seconds = Math.round((paceSeconds % 60) * 100) / 100; | ||
| return `${minutes}'${seconds.toFixed(2).padStart(5, '0')}"`; |
There was a problem hiding this comment.
페이스를 소수점 둘째 자리까지 표시하고 padStart를 사용하는 로직이 추가되었네요. seconds.toFixed(2).padStart(5, '0')는 3.5초를 03.50으로, 10.12초를 10.12로 올바르게 포맷합니다. 다만, padStart의 첫 번째 인자인 targetLength가 5로 설정되어 있는데, 초가 한 자리 수일 때(예: 3.50 -> 03.50)는 괜찮지만 두 자리 수일 때(예: 12.34)는 그대로 12.34가 되어 전체 길이가 달라집니다. 의도된 동작인지 확인이 필요해 보입니다. 만약 항상 XX.XX 형태를 원한다면 seconds.toFixed(2).toString().padStart(5, '0')와 같이 사용할 수 있습니다.
작업 내용
참고 사항