Conversation
Walkthrough테마 시스템을 새로 도입하고 30개 이상의 화면 컴포넌트에 동적 테마 지원을 추가했습니다. 동시에 푸시 알림 서비스를 재작성하고, 라우렛 화면을 업데이트하며, 네비게이션 구조와 앱 설정 파일을 변경했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User as 사용자
participant App as 앱
participant ThemeContext as ThemeContext
participant Storage as AsyncStorage
participant Screen as 화면 컴포넌트
User->>App: 앱 시작
App->>ThemeContext: ThemeProvider 초기화
ThemeContext->>Storage: 저장된 테마 로드
Storage-->>ThemeContext: 선택된 테마 반환
ThemeContext->>Screen: theme, useTheme() 제공
Screen->>Screen: 테마 기반 스타일 적용
Screen-->>User: 테마된 UI 렌더링
User->>Screen: ThemeSelectorScreen 접근
Screen->>User: 테마 옵션 표시
User->>Screen: 테마 선택
Screen->>ThemeContext: changeTheme(themeId)
ThemeContext->>Storage: 선택된 테마 저장
ThemeContext->>Screen: theme 상태 업데이트
Screen->>Screen: 새 테마로 리렌더링
Screen-->>User: 테마 변경 완료
sequenceDiagram
participant App as App.js
participant Device as expo-device
participant Permissions as 권한 시스템
participant Notifications as Notifications
participant PushService as PushNotificationService
participant Server as 서버
App->>Device: 디바이스 확인
Device-->>App: isDevice 결과
alt 실제 디바이스 (iOS)
App->>Permissions: 푸시 알림 권한 요청
Permissions-->>App: 권한 상태
alt 권한 허용됨
App->>Notifications: 푸시 토큰 얻기
Notifications-->>App: expoPushToken
App->>PushService: registerExpoPushToken()
PushService->>Server: POST /api/push-tokens (토큰 업로드)
Server-->>PushService: 성공
App->>PushService: setupNotificationListeners()
PushService->>Notifications: 알림 수신 리스너 등록
else 권한 거부됨
App->>App: 로깅만 수행
end
else 시뮬레이터 또는 Android
App->>App: 로깅만 수행
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes 분석 근거:
Pre-merge checks and finishing touches❌ Failed checks (3 warnings)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
src 코드랑 App.js 코드 변경 |
There was a problem hiding this comment.
Actionable comments posted: 20
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
src/screens/Main/AssetDetailScreen.js (1)
126-143: 테마 팔레트 비어있을 때 런타임 위험(분모 0) — 안전 가드 추가 권장theme.chart.colors 길이가 0이면 index % 0 → NaN, color가 undefined가 됩니다. 안전한 기본 팔레트를 두고 가드하세요.
다음처럼 수정 제안:
const prepareChartData = () => { @@ - return assetData.breakdown.map((item, index) => ({ + const palette = + Array.isArray(theme.chart?.colors) && theme.chart.colors.length > 0 + ? theme.chart.colors + : ["#4bc0c0", "#36a2eb", "#ff6384", "#ffcd56", "#9966ff"]; + + return assetData.breakdown.map((item, index) => ({ name: item.label, value: item.value, - color: theme.chart.colors[index % theme.chart.colors.length], + color: palette[index % palette.length], legendFontColor: theme.text.primary, legendFontSize: 12, }));src/screens/Main/SearchScreen.js (1)
30-33: 검색어 쿼리 파라미터 인코딩 누락공백/특수문자에서 400/오동작 위험. encodeURIComponent로 안전하게 인코딩하세요.
- `${API_BASE_URL}/api/stock/autocomplete/?query=${query}` + `${API_BASE_URL}/api/stock/autocomplete/?query=${encodeURIComponent(query)}`- `${API_BASE_URL}/api/stock/search/?query=${query}` + `${API_BASE_URL}/api/stock/search/?query=${encodeURIComponent(query)}`Also applies to: 55-58
src/screens/Main/TradingBuyScreen.js (1)
49-51: stock_code 쿼리 인코딩 누락기호 포함 코드/심볼에서 오동작 가능. 인코딩하세요.
- `${API_BASE_URL}trading/stock_price/?stock_code=${stockCode}` + `${API_BASE_URL}trading/stock_price/?stock_code=${encodeURIComponent(stockCode)}`src/screens/Guide/GuideScreen.js (1)
63-71: 토큰 없음 경로에서 loading 상태 해제 누락accessToken이 없으면 try/finally 밖에서 return하여 setLoading(false)가 호출되지 않습니다. 반환 전에 해제하세요.
- if (!accessToken) { + if (!accessToken) { Alert.alert("인증 오류", "토큰이 만료되었습니다. 다시 로그인해주세요."); navigation.navigate("Login"); - return; + setLoading(false); + return; }src/screens/Main/StockDetail.js (1)
345-380: 관심주식 토글 경쟁 상태 방지(빠른 연타)와 롤백 안정화요청 중 다중 탭 시 상태가 꼬일 수 있습니다. in-flight 가드 및 실패 시 확정적 롤백을 추가하세요.
+ const [favoriteLoading, setFavoriteLoading] = useState(false); const toggleFavorite = async () => { - try { + if (favoriteLoading) return; + try { + setFavoriteLoading(true); const newFavoriteState = !isFavorite; setIsFavorite(newFavoriteState); let result; if (newFavoriteState) { result = await addToWatchlist(navigation, symbol); } else { result = await removeFromWatchlist(navigation, symbol); } if (result.success) { console.log("✅ 관심주식 처리 성공:", result.message); } else { - setIsFavorite(isFavorite); + setIsFavorite(!newFavoriteState); // 확정적 롤백 Alert.alert("오류", result.message || "관심주식 처리에 실패했습니다."); } } catch (error) { console.error("❌ 관심주식 토글 오류:", error); - setIsFavorite(isFavorite); + setIsFavorite(prev => prev); // 혹은 !newFavoriteState로 롤백 Alert.alert("오류", "관심주식 처리 중 문제가 발생했습니다."); + } finally { + setFavoriteLoading(false); } };- <TouchableOpacity + <TouchableOpacity onPress={toggleFavorite} style={styles.favoriteButton} + disabled={favoriteLoading} >Also applies to: 560-569
src/screens/Auth/SignUp2Screen.js (1)
248-250: 민감 데이터 로깅 제거/가드회원가입 응답 전체를 로그로 남기면 PII 유출 위험이 있습니다. 개발 환경에서만 출력하거나 제거하세요.
- console.log("✅ 회원가입 응답 데이터:", data); + if (__DEV__) console.log("✅ 회원가입 응답 데이터:", { status: data?.status });
🧹 Nitpick comments (60)
src/utils/theme.js (1)
74-92: 차트 색상 배열 포맷 일관성
stack테마의chart.colors배열에서 들여쓰기가 일관되지 않습니다(74-91라인). 다른 테마와 달리colors:다음 줄에 배열이 시작되며, 들여쓰기 수준이 다릅니다.코드 가독성을 위해 다른 테마와 동일한 포맷을 사용하세요.
chart: { -colors: [ - '#2CAD66', - '#FFD166', - ... -] + colors: [ + '#2CAD66', '#FFD166', '#6EE69E', '#FF8FB1', + '#26C6DA', '#B39DDB', '#7FD99A', '#E85D4A', + '#81C784', '#F4E285', '#A78BD4', '#4FC3F7', + '#FFB74D', '#C8E6C9', '#F8BBD0', '#CFD8DC', + ], },src/screens/Guide/TypeExamScreen.js (1)
125-130: 로딩 상태 접근성 개선 권장로딩 인디케이터에
theme.status.success색상을 사용하고 있습니다. 일반적으로 로딩 상태는theme.accent.primary또는 전용 로딩 색상을 사용하는 것이 의미론적으로 더 적절합니다.status.success는 성공 피드백에 예약되어 있을 수 있습니다.- <ActivityIndicator size="large" color={theme.status.success} /> + <ActivityIndicator size="large" color={theme.accent.primary} />src/screens/MyPage/FAQScreen.js (2)
46-50: 주석 처리된 FAQ 항목 정리7번 FAQ 항목("랭킹은 어떻게 계산되나요?")이 주석 처리되어 있습니다. 이 항목이 더 이상 필요하지 않으면 완전히 제거하고, 추후 추가할 계획이라면 TODO 주석과 함께 이유를 명시하세요.
90-90: 로딩 인디케이터 색상 일관성로딩 인디케이터에
theme.accent.primary를 사용하고 있습니다. 이는 TypeExamScreen에서theme.status.success를 사용하는 것과 일관성이 없습니다. 프로젝트 전체에서 로딩 상태에 사용할 일관된 테마 토큰을 정의하는 것을 권장합니다.src/screens/MyPage/ChangePasswordScreen.js (1)
138-145: 버튼 비활성화 상태 색상 개선 권장버튼의 비활성화 상태에서
theme.text.disabled색상을 배경색으로 사용하고 있습니다. 일반적으로 비활성화된 버튼은 전용theme.button.disabled색상이나theme.button.primary의 불투명도를 낮춘 버전을 사용하는 것이 더 적절합니다.테마 정의에
button.disabled토큰을 추가하는 것을 고려하세요.style={[ styles.button, { - backgroundColor: loading ? theme.text.disabled : theme.button.primary, + backgroundColor: loading ? `${theme.button.primary}80` : theme.button.primary, shadowColor: theme.button.primary } ]}src/screens/MyPage/RouletteScreen.js (4)
75-95: 애니메이션 정리(cleanup) 누락컴포넌트 언마운트 시 진행 중인 Animated 시퀀스를 중단하지 않아 메모리 낭비/경고 가능성이 있습니다. cleanup에서 stopAnimation을 호출하세요.
- React.useEffect(() => { + React.useEffect(() => { const pulse = () => { Animated.sequence([ Animated.timing(pulseAnim, { toValue: 1.05, duration: 1000, easing: Easing.inOut(Easing.quad), useNativeDriver: true, }), Animated.timing(pulseAnim, { toValue: 1, duration: 1000, easing: Easing.inOut(Easing.quad), useNativeDriver: true, }), ]).start(() => { if (!spinning) pulse(); }); }; pulse(); - }, [spinning]); + return () => { + // 진행 중 애니메이션 정리 + pulseAnim.stopAnimation(); + }; + }, [spinning, pulseAnim]);
156-183: HEX 뒤에 알파값 접미 사용 패턴 점검
${theme.text.primary}1A등 8자리 HEX를 가정합니다. theme 토큰이 HEX가 아니면(예: rgb(), named color) 런타임 오류가 납니다. 전역적으로 withAlpha 유틸을 만들어 안전하게 처리하는 것을 권장합니다. 이 패턴은 본 파일 전반에 반복됩니다.Based on learnings
21-27: 가로/세로 전환 대응WHEEL_SIZE를 초기 Dimensions로 고정하면 회전 시 레이아웃이 맞지 않을 수 있습니다. useWindowDimensions로 값 재계산 또는 Dimensions change 리스너 추가를 권장합니다.
16-17: 벡터 아이콘 로딩 전략Expo 환경에서는 '@expo/vector-icons' 사용이 폰트 로딩/번들링 면에서 안전합니다. 현재 react-native-vector-icons 직접 import를 쓰므로, Dev Client/릴리즈에서 “Unrecognized font family”를 피하려면 폰트 프리로드 또는 @expo/vector-icons로의 전환을 고려하세요.
Based on learnings
src/screens/Auth/SignUp4Screen.js (1)
20-25: 배경색 적용 위치 조정ScrollView의 전체 배경엔 style이 더 적합합니다. contentContainerStyle은 콘텐츠 영역에만 적용돼 빈 공간이 보일 수 있습니다.
- <ScrollView - contentContainerStyle={[ - styles.container, - { backgroundColor: theme.background.primary } - ]} - > + <ScrollView + style={{ backgroundColor: theme.background.primary }} + contentContainerStyle={styles.container} + >src/screens/Auth/SignUp3Screen.js (3)
26-31: ref 포커스 안전 처리ref가 아직 연결되지 않은 타이밍에서 focus 호출 시 크래시 가능성이 있습니다. 옵셔널 체이닝으로 방어하세요.
- if (index < 5) { - inputs.current[index + 1].focus(); - } else { + if (index < 5) { + inputs.current[index + 1]?.focus(); + } else { verifyCode(newCode.join("")); }
57-60: 리셋 후 첫 입력칸 포커스도 안전 처리마찬가지로 옵셔널 체이닝으로 방어하세요.
- inputs.current[0].focus(); + inputs.current[0]?.focus();
49-51: 개인정보 로그 최소화 권장응답 전체 로그는 이메일 등 식별자 포함 가능성이 있어 운영 빌드에선 제거/레벨 하향을 권장합니다.
src/components/LearningProgressBar.js (2)
5-9: 미사용 import 제거Icon이 사용되지 않습니다. 정리하세요.
-import Icon from 'react-native-vector-icons/FontAwesome5';
18-24: HEX+알파 접미 패턴 주의
${theme.status.success}4D는 테마 색이 HEX일 때만 유효합니다. 전역 유틸(withAlpha)로 일관되게 처리하는 것을 권장합니다. 본 파일과 전역에 동일.Based on learnings
src/screens/Main/AssetDetailScreen.js (1)
113-117: 금액 포맷 공통 유틸로 추출해 중복 제거formatCurrency가 다른 화면과 미묘히 달라질 위험이 있습니다. utils/number.ts(또는 js)로 추출해 재사용을 권장합니다.
src/screens/MyPage/NoticeScreen.js (1)
115-151: 공지 목록은 FlatList로 전환 권장 (성능/메모리)데이터가 많아질 경우 ScrollView + map은 비효율적입니다. FlatList로 전환하면 가상화/리사이클링/스크롤 성능이 개선됩니다. keyExtractor, getItemLayout, initialNumToRender 등도 설정해주세요. 뒤로가기/공지 아이템에 accessibilityRole="button" 추가도 권장.
src/screens/Main/StockTradeScreen.js (1)
273-307: 매수/매도 버튼 색상 매핑 확인 필요현재 매수=theme.status.down, 매도=theme.status.up입니다. 의도대로(예: 매수=파랑/매도=빨강 또는 반대)인지 디자인 가이드를 재확인해주세요. 앱 전반과 일관성도 점검 바랍니다.
또한 averageLine/stockLine 등 남은 하드코드 색상도 테마로 통일하면 완성도가 높아집니다.
src/utils/ThemeContext.js (1)
46-51: Provider value 메모이제이션으로 리렌더 최적화value 객체를 useMemo로 감싸면 하위 트리 불필요 리렌더를 줄일 수 있습니다.
다음처럼 변경 제안:
+ const value = React.useMemo( + () => ({ theme, currentTheme, changeTheme, themes }), + [theme, currentTheme] + ); return ( - <ThemeContext.Provider value={{ theme, currentTheme, changeTheme, themes }}> + <ThemeContext.Provider value={value}> {children} </ThemeContext.Provider> );src/screens/Main/SearchScreen.js (2)
44-44: 비동기 상태 로그가 이전 state를 찍습니다setState 직후의 state는 최신이 아닙니다. 응답 데이터를 직접 로그로 출력하세요.
- console.log("자동완성 결과:", autoCompleteResults); + console.log("자동완성 결과:", data.results ?? data);
78-88: 타이핑 중 요청 경합/역전 방지(Abort/요청 ID 사용 권장)느린 응답이 나중에 도착해 목록이 뒤섞일 수 있습니다. AbortController 또는 요청 ID로 최신 응답만 반영하세요.
예시(요약):
- useRef로 abortController/currentReqId 보관
- fetch 시 signal 전달, effect cleanup에서 abort()
- 응답 시 reqId 일치할 때만 setAutoCompleteResults
Also applies to: 22-50
src/screens/Auth/ResetPasswordScreen.js (1)
64-66: setLoading(false) 중복 호출 제거finally에서 이미 처리됩니다. 중복 호출을 제거해 흐름을 단순화하세요.
- setLoading(false); - return; + return;src/navigation/StackNavigator.js (1)
71-71: 주석된 코드 정리불필요한 주석 블록은 추후 혼선을 유발합니다. 삭제를 권장합니다.
Also applies to: 74-78
src/screens/Main/TradingBuyScreen.js (3)
268-269: theme.status.success 키 존재 여부 확인 및 폴백 적용일부 테마에 success가 없으면 색상이 undefined가 됩니다. 폴백 색을 두세요.
컴포넌트 상단 예시:
const successColor = theme.status?.success ?? theme.accent.primary;아래 사용처 변경:
- <Text style={[styles.totalAmount, { color: theme.status.success }]}> + <Text style={[styles.totalAmount, { color: successColor }]}>- { backgroundColor: theme.status.success }, + { backgroundColor: successColor },Also applies to: 275-279
248-251: 입력 배경/글자색이 뒤바뀌었습니다보통 입력 배경은 card/secondary, 텍스트는 text.primary를 사용합니다. 가독성/일관성 개선 권장.
- style={[styles.input, { - backgroundColor: theme.text.primary, - color: theme.background.primary - }]} + style={[styles.input, { + backgroundColor: theme.background.card, + color: theme.text.primary + }]}
275-280: 비활성화 색상 테마화하드코딩된 회색 대신 테마 토큰을 사용하면 일관성↑. 동적 스타일로 단순화하세요.
- style={[ - styles.buyButton, - { backgroundColor: theme.status.success }, - (loading || priceLoading) && styles.disabledButton - ]} + style={[ + styles.buyButton, + { backgroundColor: (loading || priceLoading) ? theme.text.disabled : (theme.status?.success ?? theme.accent.primary) }, + ]}추가로 styles.disabledButton는 제거 가능.
src/screens/Guide/GuideLevel3.js (2)
11-11: 미사용 import/변수 제거Dimensions와 screenWidth가 사용되지 않습니다. 정리하세요.
- Dimensions,-const { width: screenWidth } = Dimensions.get('window');Also applies to: 24-25
63-66: 에러 Alert와 화면 내 에러 UI 중복 노출Alert와 에러 화면을 동시에 노출하면 UX가 거칠어집니다. 하나만 유지하세요(화면 내 에러 UI 권장).
Also applies to: 96-105
src/screens/Main/MainScreen.js (3)
484-526: 중첩 ScrollView로 인한 이중 스크롤/퍼포먼스 이슈바깥 ScrollView 안에 watchlist를 또 ScrollView로 감싸 이중 스크롤이 발생할 수 있습니다. 단순 목록이면 View로 대체하거나, 내부 스크롤 높이를 제한하세요.
예시 diff(내부 스크롤 제거):
- <ScrollView showsVerticalScrollIndicator={false}> + <View> {watchlist.map((stock) => ( ... ))} - </ScrollView> + </View>
408-413: 검색 아이콘 색상 하드코딩 → 테마 적용"#6B7280" 대신 테마 색을 사용하세요.
- <Search - size={18} - color="#6B7280" + <Search + size={18} + color={theme.text.tertiary} strokeWidth={2} style={styles.searchIconInInput} />
500-505: 즐겨찾기 비활성 상태의 시인성 개선비활성(미즐겨찾기) 시에도 stroke 색이 accent라 차이가 미미합니다. 미즐겨찾기 상태는 text.tertiary 등 약한 색을 권장합니다.
- <Star - size={20} - color={theme.accent.primary} - fill={stock.isFavorite ? theme.accent.primary : "transparent"} - strokeWidth={2} - /> + <Star + size={20} + color={stock.isFavorite ? theme.accent.primary : theme.text.tertiary} + fill={stock.isFavorite ? theme.accent.primary : "transparent"} + strokeWidth={2} + />src/screens/Main/TradingSellScreen.js (1)
371-371: 비활성 버튼 색상 테마화 제안disabledButton의 배경색이 하드코딩(#A0A0A0) 되어 있습니다. theme 기반 disabled 색상을 도입하는 것이 일관성에 좋습니다.
예: theme.button.disabled가 있다면 대체, 없다면
${theme.text.secondary}66같은 투명도 색상 사용을 고려.src/navigation/MainTab.js (2)
53-59: 활성/비활성 색상이 동일하여 구분 약함tabBarInactiveTintColor를 더 약한 색(예: theme.text.secondary/tertiary)로 분리하세요.
- tabBarActiveTintColor: theme.accent.primary, - tabBarInactiveTintColor: theme.accent.primary, + tabBarActiveTintColor: theme.accent.primary, + tabBarInactiveTintColor: theme.text.tertiary,
36-61: 키보드 표시 시 탭바 자동 숨김 제안입력 화면 UX 개선을 위해 tabBarHideOnKeyboard 옵션을 권장합니다.
screenOptions={{ headerShown: false, + tabBarHideOnKeyboard: true, tabBarStyle: {App.js (1)
19-19: 환영 배너 프로덕션 노출 제어 제안SHOW_WELCOME_ON_LAUNCH는 개발 시에만 활성화하는 것이 안전합니다. __DEV__나 원격 설정으로 제어를 권장합니다.
src/screens/Guide/GuideScreen.js (2)
16-16: 아이콘 라이브러리 일관성 제안 (Lucide로 통일)프로젝트 전반에서 lucide-react-native를 채택 중입니다. 본 화면의 Feather 아이콘(react-native-vector-icons)도 Lucide로 통일하면 번들/폰트 로딩을 줄이고 일관성이 개선됩니다. 예) HelpCircle, ChevronRight, ArrowRight 등.
Based on learnings
Also applies to: 58-59, 119-120, 183-183
260-279: 잠금 상태 단계의 탐색 제어이전 단계 미완료(prevComplete=false)에서도 onPress가 동작합니다. 잠금 상태에서는 비활성화하거나 안내 Alert를 띄우는 UX를 권장합니다.
src/screens/Main/StockDetail.js (2)
382-413: chartConfig 매 렌더 재생성 → useMemo로 고정불필요한 재렌더/애니메이션 리셋을 줄이기 위해 메모이즈하세요.
- const getChartConfig = () => ({ + const chartConfig = React.useMemo(() => ({ backgroundColor: theme.background.secondary, backgroundGradientFrom: theme.background.secondary, backgroundGradientTo: theme.background.secondary, decimalPlaces: 0, color: getChartColor, - labelColor: (opacity = 1) => theme.text.primary, + labelColor: (opacity = 1) => theme.text.primary, // 필요시 rgba로 변환 가능 style: { borderRadius: 16 }, propsForDots: { r: "4", strokeWidth: "2", stroke: stockData?.changeStatus === "up" ? theme.status.up : stockData?.changeStatus === "down" ? theme.status.down : theme.status.same, }, - }); + }), [theme, stockData?.changeStatus, getChartColor]);- chartConfig={getChartConfig()} + chartConfig={chartConfig}
474-498: 기간 버튼의 하드코딩 색상 제거하고 테마 사용현재
periodButtonContainer/selectedPeriodButton/periodButtonText가 고정 색상입니다. 테마 반영으로 일관성을 유지하세요.- <View style={styles.periodButtonContainer}> + <View style={[styles.periodButtonContainer, { backgroundColor: theme.background.secondary }]}> {periods.map((period) => ( <TouchableOpacity key={period.key} style={[ styles.periodButton, - selectedPeriod === period.key && styles.selectedPeriodButton, + selectedPeriod === period.key && { backgroundColor: theme.accent.primary }, ]} onPress={() => setSelectedPeriod(period.key)} > <Text style={[ styles.periodButtonText, - selectedPeriod === period.key && styles.selectedPeriodButtonText, + selectedPeriod === period.key && { color: theme.background.primary }, ]} > {period.label} </Text> </TouchableOpacity> ))} </View>스타일 객체 내 고정값(
#004455,#F074BA,#9ca3af,#FFFFFF)은 제거 권장.src/screens/Auth/SignUp2Screen.js (1)
86-87: 생년월일 검증 강화(존재하지 않는 날짜 허용 방지)정규식만으로는 2024-02-31 같은 값이 통과합니다. 실제 날짜 유효성을 확인하세요.
- const isValidDate = (d) => /^\d{4}-\d{2}-\d{2}$/.test((d || "").trim()); + const isValidDate = (d) => { + const s = (d || "").trim(); + if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) return false; + const [y, m, day] = s.split("-").map(Number); + const dt = new Date(y, m - 1, day); + return ( + dt.getFullYear() === y && + dt.getMonth() === m - 1 && + dt.getDate() === day + ); + };src/screens/Chatbot/ChatbotScreen.js (1)
187-199: TypingIndicator 애니메이션 정리 누락(언마운트 시 루프 지속 가능성)언마운트 시 애니메이션 루프를 중단하세요.
React.useEffect(() => { - const animate = () => { - Animated.sequence([ + let cancelled = false; + const animate = () => { + if (cancelled) return; + Animated.sequence([ // timings... - ]).start(() => animate()); + ]).start(() => { if (!cancelled) animate(); }); }; animate(); - }, []); + return () => { cancelled = true; }; + }, []);src/screens/Guide/TypeResultScreen.js (1)
145-161: URL 스킴 보정으로 링크 열기 실패 방지(선택)
http/https스킴이 없는 입력에 대해 보정하면 실패율이 줄어듭니다.- const trimmedUrl = url.trim(); + const trimmedUrl = url.trim(); + const withScheme = /^(https?:)?\/\//i.test(trimmedUrl) + ? trimmedUrl + : `https://${trimmedUrl}`; - Linking.canOpenURL(trimmedUrl) + Linking.canOpenURL(withScheme) .then((supported) => { if (supported) { - Linking.openURL(trimmedUrl); + Linking.openURL(withScheme);src/screens/MyPage/ThemeSelectorScreen.js (1)
74-75: StatusBar 바 색상 동적 처리라이트 테마에서도
light-content고정은 대비 저하 위험이 있습니다. 테마에 따라barStyle을 분기하세요.- <StatusBar barStyle="light-content" /> + <StatusBar barStyle={theme?.background?.primary === '#F8FAF5' ? 'dark-content' : 'light-content'} />또는 ThemeContext에서
isDark플래그를 제공해 명확히 분기.src/screens/Guide/GuideLevel1.js (2)
31-68: useFocusEffect 의 종속성/클로저 안정화fetchProgress 를 useCallback으로 감싸고, useFocusEffect는 해당 콜백을 의존하도록 조정하세요. 현재 구현은 초기 렌더의 fetchProgress 레퍼런스를 캡처해 잠재적 스테일 클로저가 될 수 있습니다.
- const fetchProgress = async () => { + const fetchProgress = React.useCallback(async () => { setLoading(true); setError(null); try { const accessToken = await getNewAccessToken(navigation); if (!accessToken) { Alert.alert('인증 오류', '토큰이 만료되었습니다. 다시 로그인해주세요.'); navigation.navigate('Login'); return; } const res = await fetch(`${API_BASE_URL}progress/level/1/content/`, { method: 'GET', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, }); if (!res.ok) throw new Error(`Level 1 content fetch failed: ${res.status}`); const data = await res.json(); setContentProgress(data?.content_progress || {}); } catch (err) { console.error('Error fetching progress:', err); setError(err.message); Alert.alert('데이터 오류', '진행도 정보를 불러오는 중 오류가 발생했습니다.'); } finally { setLoading(false); } - }; + }, [navigation]); - useFocusEffect( - React.useCallback(() => { - fetchProgress(); - }, []) - ); + useFocusEffect( + React.useCallback(() => { + fetchProgress(); + }, [fetchProgress]) + );
55-59: 에러 처리 UX 일관화 (Alert + 에러 화면 중복 알림 방지)catch 블록에서 Alert와 에러 상태 화면이 동시에 노출됩니다. Alert를 제거하고 에러 화면만 노출하거나, 반대로 Alert만 사용해 중복 피드백을 줄이는 것을 권장합니다.
- } catch (err) { - console.error('Error fetching progress:', err); - setError(err.message); - Alert.alert('데이터 오류', '진행도 정보를 불러오는 중 오류가 발생했습니다.'); - } finally { + } catch (err) { + console.error('Error fetching progress:', err); + setError(err.message); + } finally {src/screens/Auth/SignUp1Screen.js (3)
23-117: 정적 약관 데이터는 useMemo로 고정 (불필요 재생성 방지)termsData는 렌더마다 새 객체를 생성합니다. useMemo로 고정하면 리렌더 비용과 자식 렌더링을 줄일 수 있습니다.
- const termsData = { + const termsData = React.useMemo(() => ({ required1: { title: '[필수] 두둑 이용 약관', content: `**제1조 (목적)** ... 본 약관은 2025년 9월 30일부터 시행됩니다.` }, required6: { title: '[필수] 만 14세 이상입니다.', content: `본 서비스는 만 14세 이상의 사용자만 이용할 수 있습니다. ...` }, optional2: { title: '[선택] 광고성 정보 수신 동의', content: `**광고성 정보 수신 목적** ...` } - }; + }), []);
141-150: 전체 동의는 파생 상태로 계산 (상태 불일치 예방)agreements.all을 별도로 보관하지 않고, required/optional의 합성으로 매 렌더 계산하는 편이 안전합니다. 현재 구현은 동기화 코드를 유지해야 해 추후 변경에 취약합니다.
예: const allChecked = agreements.required1 && agreements.required6 && agreements.optional2; UI에서는 allChecked 사용, toggleAll은 각 항목만 토글.
193-201: 체크박스 접근성 속성 추가체크박스 터치 영역에 accessibilityRole과 accessibilityState를 부여해 스크린리더 사용성을 개선하세요.
- <TouchableOpacity onPress={() => handleCheckboxPress(key)}> + <TouchableOpacity + onPress={() => handleCheckboxPress(key)} + accessibilityRole="checkbox" + accessibilityState={{ checked: isChecked }} + accessibilityLabel={term.title} + >src/screens/Guide/StudyScreen.js (2)
267-289: 완료 버튼이 스크롤에 포함되어 고정되지 않음 (하단 고정으로 이동 권장)완료 버튼 컨테이너가 ScrollView 내부에 있어 콘텐츠 길이에 따라 화면 하단에 고정되지 않습니다. ScrollView 밖(동일 레벨)으로 옮기면 하단 고정이 자연스럽게 동작합니다.
<ScrollView ref={scrollViewRef} style={styles.scrollArea} contentContainerStyle={styles.scrollContent} onScroll={handleScroll} scrollEventThrottle={16} showsVerticalScrollIndicator={false} > <View style={styles.contentContainer}> <Markdown style={{...markdownStyles, body: {...markdownStyles.body, fontSize}}}>{content}</Markdown> </View> - - {/* Complete Button - 페이지 하단에 고정 */} - <View style={styles.completeButtonContainer}> - <TouchableOpacity - style={[styles.completeButton, completing && styles.completingButton]} - onPress={handleComplete} - disabled={completing} - activeOpacity={0.8} - > - {completing ? ( - <View style={styles.completingContent}> - <ActivityIndicator color="#fff" size="small" /> - <Text style={styles.completingText}>처리 중...</Text> - </View> - ) : ( - <View style={styles.buttonContent}> - <Icon name="check-circle" size={20} color="#fff" /> - <Text style={styles.buttonText}>학습을 완료했어요</Text> - </View> - )} - </TouchableOpacity> - </View> </ScrollView> + + {/* Complete Button - 화면 하단 고정 */} + <View style={styles.completeButtonContainer}> + <TouchableOpacity + style={[styles.completeButton, completing && styles.completingButton]} + onPress={handleComplete} + disabled={completing} + activeOpacity={0.8} + > + {completing ? ( + <View style={styles.completingContent}> + <ActivityIndicator color="#fff" size="small" /> + <Text style={styles.completingText}>처리 중...</Text> + </View> + ) : ( + <View style={styles.buttonContent}> + <Icon name="check-circle" size={20} color="#fff" /> + <Text style={styles.buttonText}>학습을 완료했어요</Text> + </View> + )} + </TouchableOpacity> + </View>
47-88: 데이터 요청 취소로 언마운트 후 setState 경고 예방언마운트 시 fetch를 AbortController로 취소하거나 isMounted 플래그로 setState를 방지하는 것을 권장합니다. 스크린 전환/백그라운드 복귀 시 안전성이 좋아집니다.
간단 예시:
- 컨트롤러 생성 → fetch에 signal 전달
- useEffect cleanup에서 abort()
- catch에서 err.name === 'AbortError' 분기 처리
src/screens/Guide/GuideLevel2.js (3)
31-68: useFocusEffect와 fetchProgress의 참조 안정화Level1과 동일하게 fetchProgress를 useCallback으로 감싸고, useFocusEffect의 의존성에 포함해 스테일 클로저를 피하세요.
- const fetchProgress = async () => { + const fetchProgress = React.useCallback(async () => { setLoading(true); setError(null); try { const accessToken = await getNewAccessToken(navigation); if (!accessToken) { Alert.alert('인증 오류', '토큰이 만료되었습니다. 다시 로그인해주세요.'); navigation.navigate('Login'); return; } const res = await fetch(`${API_BASE_URL}progress/level/2/content/`, { method: 'GET', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, }); if (!res.ok) throw new Error(`Level 2 content fetch failed: ${res.status}`); const data = await res.json(); setContentProgress(data?.content_progress || {}); } catch (err) { console.error('Error fetching progress:', err); setError(err.message); Alert.alert('데이터 오류', '진행도 정보를 불러오는 중 오류가 발생했습니다.'); } finally { setLoading(false); } - }; + }, [navigation]); - useFocusEffect( - React.useCallback(() => { - fetchProgress(); - }, []) - ); + useFocusEffect( + React.useCallback(() => { + fetchProgress(); + }, [fetchProgress]) + );
55-61: 에러 Alert 중복 노출 정리Alert와 에러 상태 화면을 동시에 보여 중복 피드백이 발생합니다. 하나로 통일해 사용자 경험을 간결하게 유지하세요.
- } catch (err) { - console.error('Error fetching progress:', err); - setError(err.message); - Alert.alert('데이터 오류', '진행도 정보를 불러오는 중 오류가 발생했습니다.'); - } finally { + } catch (err) { + console.error('Error fetching progress:', err); + setError(err.message); + } finally {
98-104: 중복 로직 공통화 제안 (Level1/2/3용 훅/유틸)Level1/2/3에서 동일한 진행도 로직이 반복됩니다. useGuideProgress(level) 같은 커스텀 훅으로 fetch/에러/entries/firstIncomplete를 공통화하면 유지보수가 쉬워집니다.
src/services/PushNotificationService.ios.js (2)
53-60: 네트워크 호출에 타임아웃/취소(AbortController) 추가 권장.모바일 네트워크 특성상 fetch 무한대기 방지를 위해 타임아웃을 적용하세요.
- const res = await fetch(`${API_BASE_URL}api/push-tokens`, { + const res = await fetchWithTimeout(`${API_BASE_URL}api/push-tokens`, { method: "POST", headers: { "Content-Type": "application/json", ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, body: JSON.stringify(body), });- const res = await fetch(`${API_BASE_URL}api/push-tokens`, { + const res = await fetchWithTimeout(`${API_BASE_URL}api/push-tokens`, { method: "DELETE", headers: { "Content-Type": "application/json", ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, body: JSON.stringify({ token }), });추가: 파일 상단 유틸에 타임아웃 헬퍼를 정의하세요.
function fetchWithTimeout(resource, options = {}, timeoutMs = 10000) { const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeoutMs); return fetch(resource, { ...options, signal: controller.signal }) .finally(() => clearTimeout(id)); }Also applies to: 79-86
25-29: AsyncStorage 예외를 완전 무시하지 말고 개발환경에서만 경고를 남기세요.디버깅 가시성을 위해 최소 dev 로깅을 권장합니다.
async function saveLocalToken(token) { try { await AsyncStorage.setItem("pushToken", token); - } catch {} + } catch (e) { + if (__DEV__) console.warn("[Push] 로컬 토큰 저장 실패:", e?.message || e); + } } async function loadLocalToken() { try { return (await AsyncStorage.getItem("pushToken")) || null; - } catch { - return null; - } + } catch (e) { + if (__DEV__) console.warn("[Push] 로컬 토큰 로드 실패:", e?.message || e); + return null; + } }Also applies to: 31-36
src/screens/MyPage/MyPageScreen.js (5)
164-172: unregisterPushToken에 navigation 인자 제거 (API 시그니처 정리).현재 구현은 인자를 무시하지만, 혼란을 줄이기 위해 호출부에서 제거하세요.
- const pushUnregisterSuccess = await unregisterPushToken(navigation); + const pushUnregisterSuccess = await unregisterPushToken();- await unregisterPushToken(navigation); + await unregisterPushToken();Also applies to: 260-264
53-68: 인증 처리 로직 일원화 권장 (fetchWithAuth 재사용).getNewAccessToken + 수동 헤더 설정 대신, 프로젝트 전반에서 사용하는 fetchWithAuth 유틸로 교체하면 401 처리/토큰 갱신 로직이 일관됩니다.
필요하면 fetchWithAuth 기반으로 이 블록을 교체하는 패치를 제안할게요.
347-352: 상단 Safe Area 반영 (노치/다이내믹 아일랜드 대응).고정 paddingTop(50) 대신 insets.top을 적용하세요. 로딩/메인 컨테이너 모두에 동일하게 반영 권장.
- <View style={[styles.container, { - backgroundColor: theme.background.primary, - justifyContent: "center" - }]}> + <View style={[styles.container, { + backgroundColor: theme.background.primary, + justifyContent: "center", + paddingTop: (insets?.top || 0) + 16 + }]}>- <View style={[styles.container, { backgroundColor: theme.background.primary }]}> + <View style={[styles.container, { + backgroundColor: theme.background.primary, + paddingTop: (insets?.top || 0) + 16 + }]}>container: { flex: 1, - paddingHorizontal: 20, - paddingTop: 50, + paddingHorizontal: 20, },Also applies to: 357-367, 515-517
401-404: 별명 비로딩 상태 문구 개선.aliasLoading이 아닐 때는 “불러오는 중” 대신 ‘별명이 아직 없습니다’ 등 상태에 맞는 문구가 자연스럽습니다.
- <Text style={[styles.mbtiAliasEmpty, { color: theme.text.tertiary }]}> - 별명을 불러오는 중... - </Text> + <Text style={[styles.mbtiAliasEmpty, { color: theme.text.tertiary }]}> + 별명이 아직 없습니다 + </Text>
415-417: 이메일 마스킹 적용으로 프라이버시 개선.UI에 직접 노출되는 이메일은 마스킹하는 편이 안전합니다. 이미 유틸이 있으니 활용하세요.
- {userInfo.email} + {maskEmail(userInfo.email)}Also applies to: 108-115
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (9)
.expo/web/cache/production/images/iconsuniversal-icon/iconsuniversal-icon-b4d49e7392f44b0e1b4fd25a5b828b2bf3fd57e856a1c968620b71d61659e519-cover-#ffffff/App-Icon-1024x1024@1x.pngis excluded by!**/*.png.expo/web/cache/production/images/splash-ios/splash-ios-b4d49e7392f44b0e1b4fd25a5b828b2bf3fd57e856a1c968620b71d61659e519-contain/icon_undefined.pngis excluded by!**/*.pngpackage-lock.jsonis excluded by!**/package-lock.jsonsrc/assets/fonts/Pretendard-Bold.otfis excluded by!**/*.otfsrc/assets/fonts/Pretendard-Medium.otfis excluded by!**/*.otfsrc/assets/fonts/Pretendard-Regular.otfis excluded by!**/*.otfsrc/assets/fonts/Pretendard-SemiBold.otfis excluded by!**/*.otfsrc/assets/fonts/Pretendard-Thin.otfis excluded by!**/*.otfsrc/assets/rainbow.pngis excluded by!**/*.png
📒 Files selected for processing (43)
.expo/devices.json(1 hunks).expo/settings.json(1 hunks)App.js(1 hunks)app.config.js(0 hunks)app.json(3 hunks)package.json(3 hunks)src/components/LearningProgressBar.js(2 hunks)src/navigation/MainTab.js(4 hunks)src/navigation/StackNavigator.js(2 hunks)src/screens/Auth/FindIdScreen.js(0 hunks)src/screens/Auth/FindPasswordScreen.js(1 hunks)src/screens/Auth/LoginScreen.js(3 hunks)src/screens/Auth/ResetPasswordScreen.js(5 hunks)src/screens/Auth/SignUp1Screen.js(2 hunks)src/screens/Auth/SignUp2Screen.js(12 hunks)src/screens/Auth/SignUp3Screen.js(6 hunks)src/screens/Auth/SignUp4Screen.js(2 hunks)src/screens/Chatbot/ChatbotScreen.js(2 hunks)src/screens/Guide/GuideLevel1.js(6 hunks)src/screens/Guide/GuideLevel2.js(6 hunks)src/screens/Guide/GuideLevel3.js(7 hunks)src/screens/Guide/GuideScreen.js(2 hunks)src/screens/Guide/StudyScreen.js(2 hunks)src/screens/Guide/TypeExamScreen.js(3 hunks)src/screens/Guide/TypeResultScreen.js(9 hunks)src/screens/Main/AssetDetailScreen.js(12 hunks)src/screens/Main/MainScreen.js(12 hunks)src/screens/Main/SearchScreen.js(5 hunks)src/screens/Main/StockDetail.js(10 hunks)src/screens/Main/StockTradeScreen.js(7 hunks)src/screens/Main/TradingBuyScreen.js(6 hunks)src/screens/Main/TradingSellScreen.js(5 hunks)src/screens/MyPage/ChangePasswordScreen.js(5 hunks)src/screens/MyPage/EditUserInfoScreen.js(1 hunks)src/screens/MyPage/FAQScreen.js(5 hunks)src/screens/MyPage/MyPageScreen.js(9 hunks)src/screens/MyPage/NoticeScreen.js(3 hunks)src/screens/MyPage/RouletteScreen.js(3 hunks)src/screens/MyPage/ThemeSelectorScreen.js(1 hunks)src/services/PushNotificationService.ios.js(1 hunks)src/utils/ThemeContext.js(1 hunks)src/utils/point.js(1 hunks)src/utils/theme.js(1 hunks)
💤 Files with no reviewable changes (2)
- app.config.js
- src/screens/Auth/FindIdScreen.js
🧰 Additional context used
🧬 Code graph analysis (36)
src/utils/theme.js (1)
src/screens/MyPage/ThemeSelectorScreen.js (4)
themes(395-696)themes(395-696)defaultTheme(698-698)defaultTheme(698-698)
src/screens/MyPage/NoticeScreen.js (2)
src/utils/ThemeContext.js (1)
theme(10-10)src/screens/MyPage/FAQScreen.js (2)
toggleExpand(78-80)expandedId(59-59)
src/screens/Auth/ResetPasswordScreen.js (1)
src/utils/ThemeContext.js (1)
theme(10-10)
src/utils/ThemeContext.js (1)
src/utils/theme.js (4)
defaultTheme(327-327)defaultTheme(327-327)themes(3-324)themes(3-324)
src/screens/Auth/SignUp2Screen.js (2)
src/screens/Auth/FindPasswordScreen.js (10)
useTheme(25-25)seeConfirmPassword(38-38)confirmPassword(36-36)scrollRef(45-45)keyboardVisible(48-48)keyboardHeight(49-49)bottomSpacer(66-69)styles(475-582)strengthText(99-99)src/utils/ThemeContext.js (1)
theme(10-10)
src/screens/Auth/FindPasswordScreen.js (3)
src/screens/Auth/SignUp2Screen.js (9)
scrollRef(58-58)keyboardVisible(60-60)keyboardHeight(61-61)bottomSpacer(78-81)validateEmail(83-84)passwordValid(88-94)passwordStrength(96-109)strengthText(271-271)styles(662-764)src/components/EyeClosed.js (1)
EyeClosed(4-15)src/components/EyeOpen.js (1)
EyeOpen(4-11)
src/screens/MyPage/ThemeSelectorScreen.js (2)
src/utils/ThemeContext.js (3)
changeTheme(30-44)theme(10-10)currentTheme(9-9)src/utils/theme.js (4)
themes(3-324)themes(3-324)defaultTheme(327-327)defaultTheme(327-327)
src/screens/Main/TradingBuyScreen.js (1)
src/utils/ThemeContext.js (1)
theme(10-10)
src/screens/Main/TradingSellScreen.js (2)
src/screens/Main/TradingBuyScreen.js (4)
getChangeColor(163-167)getChangeSymbol(169-173)stock(22-22)styles(296-414)src/utils/ThemeContext.js (1)
theme(10-10)
src/utils/point.js (5)
src/utils/apiConfig.js (2)
response(69-75)data(81-81)src/utils/token.js (3)
response(27-31)response(82-86)response(181-188)src/utils/account.js (3)
response(10-16)data(27-27)text(19-19)src/utils/mbtiType.js (3)
response(9-15)data(27-27)text(17-17)src/utils/user.js (5)
response(10-16)response(53-60)data(27-27)text(19-19)text(63-63)
src/screens/Auth/SignUp1Screen.js (3)
src/utils/ThemeContext.js (1)
theme(10-10)src/components/CheckBoxChecked.js (1)
CheckBoxChecked(30-39)src/components/CheckBoxUnchecked.js (1)
CheckBoxChecked(20-37)
src/screens/Auth/SignUp3Screen.js (1)
src/utils/ThemeContext.js (1)
theme(10-10)
src/screens/Main/StockDetail.js (2)
src/screens/Main/AssetDetailScreen.js (2)
screenWidth(19-19)useTheme(23-23)src/utils/ThemeContext.js (1)
theme(10-10)
src/screens/MyPage/MyPageScreen.js (5)
src/utils/mbtiType.js (4)
fetchUserMbtiType(5-50)fetchUserMbtiType(5-50)response(9-15)data(27-27)src/utils/token.js (9)
accessToken(7-7)accessToken(129-129)accessToken(171-171)getNewAccessToken(126-153)getNewAccessToken(126-153)response(27-31)response(82-86)response(181-188)src/services/PushNotificationService.js (5)
accessToken(80-80)response(93-104)response(160-168)unregisterPushToken(151-191)unregisterPushToken(151-191)src/utils/apiConfig.js (4)
response(69-75)API_BASE_URL(1-2)API_BASE_URL(1-2)data(81-81)src/utils/user.js (5)
response(10-16)response(53-60)data(27-27)fetchUserInfo(6-46)fetchUserInfo(6-46)
src/screens/Auth/SignUp4Screen.js (2)
src/screens/Guide/TutorialScreen.js (1)
styles(172-221)src/utils/ThemeContext.js (1)
theme(10-10)
src/components/LearningProgressBar.js (1)
src/utils/ThemeContext.js (1)
theme(10-10)
src/screens/Chatbot/ChatbotScreen.js (4)
src/navigation/MainTab.js (2)
useTheme(20-20)insets(23-23)src/screens/Guide/GuideScreen.js (3)
useTheme(33-33)insets(36-36)tabBarHeight(37-37)src/utils/chatbotReply.js (2)
chatbotReply(4-25)chatbotReply(4-25)src/utils/ThemeContext.js (1)
theme(10-10)
src/screens/Main/AssetDetailScreen.js (2)
src/screens/Main/MainScreen.js (4)
useTheme(36-36)fetchAssetData(154-210)formatCurrency(295-298)assetData(44-44)src/utils/ThemeContext.js (1)
theme(10-10)
src/screens/Guide/TypeExamScreen.js (2)
src/components/LearningProgressBar.js (1)
useTheme(12-12)src/utils/ThemeContext.js (1)
theme(10-10)
src/screens/Auth/LoginScreen.js (2)
src/utils/token.js (2)
password(69-69)src/utils/ThemeContext.js (1)
theme(10-10)
src/screens/MyPage/RouletteScreen.js (2)
src/utils/point.js (2)
increaseBalance(4-52)increaseBalance(4-52)src/utils/ThemeContext.js (1)
theme(10-10)
src/screens/Main/StockTradeScreen.js (2)
src/utils/ThemeContext.js (1)
theme(10-10)src/components/RecommendedStock.js (1)
styles(240-310)
src/services/PushNotificationService.ios.js (2)
src/utils/token.js (3)
accessToken(7-7)accessToken(129-129)accessToken(171-171)src/utils/apiConfig.js (2)
API_BASE_URL(1-2)API_BASE_URL(1-2)
src/screens/Main/SearchScreen.js (1)
src/utils/ThemeContext.js (1)
theme(10-10)
src/screens/Guide/GuideLevel1.js (2)
src/screens/Guide/GuideLevel2.js (8)
contentProgress(28-28)error(29-29)fetchProgress(31-62)navigation(24-24)handleRetry(70-70)styles(158-247)entries(99-101)firstIncomplete(103-103)src/screens/Guide/GuideLevel3.js (8)
contentProgress(30-30)error(31-31)fetchProgress(33-69)navigation(27-27)handleRetry(77-79)styles(179-279)entries(108-110)firstIncomplete(112-112)
src/screens/Guide/StudyScreen.js (2)
src/utils/token.js (5)
accessToken(7-7)accessToken(129-129)accessToken(171-171)getNewAccessToken(126-153)getNewAccessToken(126-153)src/utils/apiConfig.js (2)
API_BASE_URL(1-2)API_BASE_URL(1-2)
App.js (3)
src/services/PushNotificationService.ios.js (6)
Notifications(116-118)Notifications(154-154)setupNotificationListeners(176-188)res(53-60)res(79-86)registerExpoPushToken(123-144)src/services/PushNotificationService.js (3)
Notifications(33-33)setupNotificationListeners(193-220)setupNotificationListeners(193-220)src/utils/ThemeContext.js (2)
ThemeProvider(8-51)ThemeProvider(8-51)
src/screens/Guide/GuideLevel2.js (2)
src/screens/Guide/GuideLevel1.js (10)
insets(25-25)loading(27-27)contentProgress(28-28)error(29-29)fetchProgress(31-62)navigation(24-24)handleRetry(70-70)styles(150-239)entries(95-97)firstIncomplete(99-99)src/screens/Guide/GuideLevel3.js (11)
insets(28-28)loading(29-29)contentProgress(30-30)error(31-31)fetchProgress(33-69)navigation(27-27)handleRetry(77-79)handleChapterPress(81-86)styles(179-279)entries(108-110)firstIncomplete(112-112)
src/screens/MyPage/FAQScreen.js (2)
src/utils/ThemeContext.js (1)
theme(10-10)src/screens/MyPage/NoticeScreen.js (2)
toggleExpand(55-57)expandedId(24-24)
src/screens/Main/MainScreen.js (4)
src/screens/Main/StockDetail.js (1)
toggleFavorite(345-380)src/utils/hantuToken.js (5)
initializeHantuToken(188-203)initializeHantuToken(188-203)scheduleTokenRefresh(237-245)scheduleTokenRefresh(237-245)result(192-192)src/utils/account.js (2)
fetchUserBalance(6-54)fetchUserBalance(6-54)src/utils/ThemeContext.js (1)
theme(10-10)
src/screens/Guide/GuideLevel3.js (4)
src/screens/Main/MainScreen.js (2)
screenWidth(32-32)styles(541-756)src/screens/Guide/StudyScreen.js (7)
screenWidth(21-21)navigation(25-25)insets(26-26)loading(37-37)error(39-39)handleRetry(144-146)styles(357-622)src/screens/Guide/GuideLevel1.js (10)
navigation(24-24)insets(25-25)loading(27-27)contentProgress(28-28)error(29-29)fetchProgress(31-62)handleRetry(70-70)styles(150-239)entries(95-97)firstIncomplete(99-99)src/screens/Guide/GuideLevel2.js (11)
navigation(24-24)insets(25-25)loading(27-27)contentProgress(28-28)error(29-29)fetchProgress(31-62)handleRetry(70-70)handleChapterPress(72-77)styles(158-247)entries(99-101)firstIncomplete(103-103)
src/screens/MyPage/ChangePasswordScreen.js (1)
src/utils/ThemeContext.js (1)
theme(10-10)
src/navigation/MainTab.js (2)
src/screens/Main/MainScreen.js (2)
useTheme(36-36)MainScreen(34-539)src/utils/ThemeContext.js (1)
theme(10-10)
src/screens/Guide/TypeResultScreen.js (1)
src/utils/ThemeContext.js (1)
theme(10-10)
src/navigation/StackNavigator.js (1)
src/screens/MyPage/ThemeSelectorScreen.js (1)
ThemeSelectorScreen(21-198)
src/screens/Guide/GuideScreen.js (2)
src/components/LearningProgressBar.js (3)
useTheme(12-12)styles(34-58)LearningProgressBar(10-32)src/utils/ThemeContext.js (1)
theme(10-10)
🔇 Additional comments (22)
src/utils/point.js (2)
20-30: 정교한 400 응답 처리 도입이 돋보입니다.이미 사용한 케이스를
already_used_today로 명확히 분기해 후속 UI 처리가 쉬워졌어요.
41-50: 에러 코드 표준화 덕분에 복구 흐름이 한결 단순해졌습니다.파싱 오류와 네트워크 오류를 명시적으로 구분해주는 구조가 호출부 예외 처리에 큰 도움이 됩니다.
app.json (2)
5-5: 공개 owner 필드 추가
expo.owner필드가 추가되어 프로젝트 소유자가 공개적으로 노출됩니다. 이것이 의도된 것인지 확인하세요.
22-22: 빌드 번호 대폭 증가 확인iOS 빌드 번호가 7에서 22로 15단계 증가했습니다. 이러한 큰 증가폭이 의도된 것인지 확인하세요. 중간 빌드를 건너뛴 경우, App Store Connect에서 빌드 이력이 일관되게 관리되고 있는지 검증하세요.
src/screens/Guide/TypeExamScreen.js (1)
13-19: 테마 통합 구현 우수
useTheme훅을 올바르게 임포트하고 사용하여 컴포넌트 전체에 일관된 테마 기반 스타일링을 적용했습니다. 로딩 상태, 진행률 표시, 질문 UI 등 모든 영역에서 하드코딩된 색상이 테마 토큰으로 대체되었습니다.src/screens/MyPage/FAQScreen.js (1)
12-14: 테마 통합 구현 우수
useTheme훅을 통해 테마 토큰을 일관되게 적용했습니다. 컨테이너, 버튼, 텍스트, FAQ 항목 등 모든 UI 요소가 동적 테마 색상을 사용합니다.src/screens/MyPage/ChangePasswordScreen.js (1)
16-20: 테마 통합 구현 우수
useTheme훅을 사용하여 모든 하드코딩된 색상을 테마 토큰으로 대체했습니다. 입력 필드, 버튼, 로딩 상태 등 모든 UI 요소가 동적 테마를 지원합니다.src/utils/theme.js (1)
29-35:chart.colors배열이 모든 테마에서 16개로 일관됩니다. 추가 검증 로직은 필요 없습니다.package.json (2)
7-8: expo run 스크립트 전환 검토"expo run:android/ios"는 네이티브 프로젝트를 생성·빌드합니다. Dev Client 사용이 전제됩니다. 순수 Managed 워크플로만 의도라면 기존 "expo start --android/ios" 또는 dev-client용 스크립트(예: "expo start --dev-client") 추가를 고려하세요.
16-33: Expo SDK 53 호환성 확인 완료
현재 사용 중인 React 19.0.0 및 React Native 0.79.5 조합은 Expo SDK 53 공식 호환 매트릭스와 일치하므로 추가 조치 불필요합니다.src/screens/MyPage/RouletteScreen.js (1)
114-116: 보상 인덱싱 계산 검증 요청선택 로직(selected 계산)이 포인터 기준/세그먼트 기준 각도 정의에 따라 오프바이원 위험이 있습니다. 테스트를 통해 모든 세그먼트가 균등히 선택되는지 확인해주세요.
src/screens/Main/StockTradeScreen.js (1)
184-186: 변동 색상 테마 연동 LGTMstatus 키 기반으로 안전한 폴백(same) 포함. 테마 연동 적절합니다.
src/screens/Auth/LoginScreen.js (1)
171-176: 키보드 대응 및 테마 적용 구성 깔끔합니다KeyboardAvoidingView/ScrollView/Animated 조합과 테마 연동 적절합니다.
src/screens/Auth/ResetPasswordScreen.js (2)
93-108: 테마 반영 전반적으로 적절합니다배경/텍스트/액센트 적용 일관성 좋습니다.
45-47: 리뷰 의견이 부정확합니다.ResetPasswordScreen.js 라인 46의 코드는 이미 올바르고 일관된 패턴을 따르고 있습니다. API_BASE_URL이
https://...app/로 정의되어 있고,${API_BASE_URL}users/password_reset/confirm/은 올바른 URLhttps://...app/users/password_reset/confirm/을 생성합니다.이 패턴은 point.js, watchList.js, PushNotificationService.js 등 코드베이스 대부분에서 사용하는 일관된 방식입니다. 슬래시 중복이나 누락 위험은 없으므로 수정이 필요하지 않습니다.
Likely an incorrect or invalid review comment.
src/navigation/StackNavigator.js (1)
35-36: ThemeSelector 라우트 추가 적절합니다Import/Screen 등록이 일관되고 headerShown 글로벌 옵션과도 충돌 없습니다.
Also applies to: 73-79
src/screens/Main/TradingBuyScreen.js (1)
124-126: POST Content-Type 확인 필요fetchWithAuth가 기본으로 설정하지 않으면 서버가 JSON을 파싱하지 못할 수 있습니다. 헤더 추가 여부를 확인하세요.
추가가 필요하면:
const response = await fetchWithAuth( `${API_BASE_URL}trading/trade/`, { method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify(orderData), }, navigation );src/screens/Guide/GuideLevel3.js (1)
71-75: 데이터 로딩/네비/터치 처리 전반 LGTMuseFocusEffect 재로딩, 안전 영역 반영, 현재/완료/잠금 상태 렌더링이 명확합니다.
Also applies to: 114-127, 156-168
src/screens/Main/StockDetail.js (1)
266-269: 날짜 파싱 안정성 검증 필요
백엔드chart_data.date포맷(ISO vs YYYYMMDD) 확인 후new Date(item.date)가 NaN을 반환하지 않도록 포맷 감지 및 파싱 로직(parseDate) 도입을 권장합니다.src/services/PushNotificationService.ios.js (2)
104-110: EAS projectId 획득 방식 환경 확인 필요.빌드/런타임에서 Constants.easConfig 또는 expoConfig.extra.eas.projectId 가 반드시 채워지는지 확인해주세요. 누락 시 토큰 등록이 항상 skip 됩니다.
176-187: 알림 리스너 등록/해제 처리 깔끔합니다.구독 객체를 보관 후 remove 호출로 누수 없이 정리하는 방식 LGTM입니다.
src/screens/MyPage/MyPageScreen.js (1)
474-503: 네비게이션 라우트 존재 확인 필요."ThemeSelector", "Notice", "FAQ", "ChangePassword" 스크린이 네비게이터에 실제로 등록되어 있는지 점검해주세요. 미등록 시 런타임 에러 발생합니다.
| "devices": [ | ||
| { | ||
| "installationId": "F0B2F989-D042-4D9B-BE7C-8D09B00BD4C5", | ||
| "lastUsed": 1758012043849 | ||
| } | ||
| ] |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
로컬 디바이스 추적 데이터 커밋 확인
이 파일은 개발자의 로컬 디바이스 정보를 추적합니다. 일반적으로 .expo/ 디렉토리는 개발자별 로컬 데이터를 포함하므로 버전 관리에서 제외되어야 합니다.
또한, lastUsed 타임스탬프(1758012043849)가 2025년 10월 이후의 미래 날짜를 나타내는 것으로 보이는데, 이는 현재 날짜(2025년 10월)와 일치하지 않을 수 있습니다. 테스트 데이터인지 확인하세요.
🏁 Script executed:
#!/bin/bash
# .gitignore에 .expo가 포함되어 있는지 확인
if [ -f ".gitignore" ]; then
if grep -q "\.expo" .gitignore; then
echo ".gitignore에 .expo가 포함되어 있습니다"
else
echo "경고: .gitignore에 .expo가 없습니다"
fi
else
echo ".gitignore 파일을 찾을 수 없습니다"
fiLength of output: 27
.expo/ 디렉토리 버전 관리 제외 및 devices.json 제거 필요
.gitignore에.expo/추가- 커밋된
.expo/devices.json파일 삭제 lastUsed타임스탬프(1758012043849)가 미래 날짜이므로 테스트 데이터인지 확인 및 수정
🤖 Prompt for AI Agents
In .expo/devices.json around lines 2 to 7: this file under .expo/ was committed
but the .expo/ directory should be ignored; add ".expo/" to .gitignore, remove
the committed .expo/devices.json from the repository (git rm --cached or delete
+ commit) so it no longer tracks, and verify the lastUsed value (1758012043849)
— if it was test data or a future timestamp, replace with a realistic epoch
millis or remove the file entirely before committing the cleanup.
| { | ||
| "hostType": "lan", | ||
| "lanType": "ip", | ||
| "dev": true, | ||
| "minify": false, | ||
| "urlRandomness": null, | ||
| "https": false | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
개발 환경 설정 파일 커밋 확인
이 파일은 로컬 개발 환경 설정을 포함하고 있습니다 (dev: true, minify: false, https: false). 일반적으로 .expo/ 디렉토리는 .gitignore에 포함되어야 하며, 개발자별 설정이 저장소에 커밋되지 않아야 합니다.
프로젝트의 .gitignore 파일에 .expo/ 항목이 포함되어 있는지 확인하고, 이 파일을 커밋하는 것이 의도된 것인지 검증하세요.
🏁 Script executed:
#!/bin/bash
# .gitignore에 .expo가 포함되어 있는지 확인
if [ -f ".gitignore" ]; then
if grep -q "\.expo" .gitignore; then
echo ".gitignore에 .expo가 포함되어 있습니다"
else
echo "경고: .gitignore에 .expo가 없습니다"
fi
else
echo ".gitignore 파일을 찾을 수 없습니다"
fiLength of output: 27
.expo/settings.json 파일을 .gitignore에 추가하세요
.expo/ 디렉토리가 .gitignore에 포함되어 있지 않습니다. settings.json은 개발 환경 설정 파일(dev: true, minify: false, https: false)이며, 이러한 로컬 설정 파일은 버전 관리 대상에서 제외되어야 합니다. .gitignore에 .expo/ 항목을 추가하거나, 이미 커밋된 경우 저장소에서 제거하세요.
🤖 Prompt for AI Agents
In .expo/settings.json (lines 1-8): this is a local dev config that should not
be committed; add a .expo/ entry to .gitignore (or ensure .expo/ exists in
.gitignore) and if the directory was already committed remove it from the repo
index without deleting local files (e.g., git rm --cached -r .expo && commit the
change), then push so the settings.json is no longer tracked.
| Notifications.setNotificationHandler({ | ||
| handleNotification: async () => { | ||
| console.log("[Push] handleNotification called (foreground display enabled)"); | ||
| return { | ||
| shouldShowBanner: true, | ||
| shouldShowList: true, | ||
| shouldPlaySound: true, | ||
| shouldSetBadge: true, | ||
| }; | ||
| }, | ||
| }); |
There was a problem hiding this comment.
expo-notifications Notification Handler 키 사용 오류
Expo는 shouldShowAlert/shouldPlaySound/shouldSetBadge를 기대합니다. shouldShowBanner/shouldShowList는 무시될 수 있습니다.
Notifications.setNotificationHandler({
handleNotification: async () => {
- console.log("[Push] handleNotification called (foreground display enabled)");
- return {
- shouldShowBanner: true,
- shouldShowList: true,
- shouldPlaySound: true,
- shouldSetBadge: true,
- };
+ console.log("[Push] handleNotification (fg alert enabled)");
+ return {
+ shouldShowAlert: true,
+ shouldPlaySound: true,
+ shouldSetBadge: true,
+ };
},
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Notifications.setNotificationHandler({ | |
| handleNotification: async () => { | |
| console.log("[Push] handleNotification called (foreground display enabled)"); | |
| return { | |
| shouldShowBanner: true, | |
| shouldShowList: true, | |
| shouldPlaySound: true, | |
| shouldSetBadge: true, | |
| }; | |
| }, | |
| }); | |
| Notifications.setNotificationHandler({ | |
| handleNotification: async () => { | |
| console.log("[Push] handleNotification (fg alert enabled)"); | |
| return { | |
| shouldShowAlert: true, | |
| shouldPlaySound: true, | |
| shouldSetBadge: true, | |
| }; | |
| }, | |
| }); |
🤖 Prompt for AI Agents
In App.js around lines 25 to 35, the Notification handler is returning keys
(shouldShowBanner, shouldShowList, shouldPlaySound, shouldSetBadge) that Expo
ignores; replace them with the expected keys shouldShowAlert, shouldPlaySound,
and shouldSetBadge (and/or shouldSetBadge as needed) so the notification display
behavior works correctly; update the returned object to include shouldShowAlert:
true, shouldPlaySound: true, shouldSetBadge: true (and remove or stop relying on
shouldShowBanner/shouldShowList).
| { | ||
| "expo": { | ||
| "name": "Doodook", | ||
| "name": "두둑", |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
앱 이름 현지화 확인
앱 이름이 "Doodook"에서 "두둑"으로 한글로 변경되었습니다. App Store 및 Play Store에서의 표시 이름이 의도대로 변경되는지 확인하세요.
🏁 Script executed:
#!/bin/bash
# 앱 이름이 다른 구성 파일에서도 일관되게 사용되는지 확인
rg -n "Doodook" --type json --type js -g '!node_modules/**'Length of output: 131
앱 이름 일관성 수정 요청
src/screens/Auth/SignUp1Screen.js 27행 등에서 '두둑(Doodook)'을 '두둑'으로 변경하세요. App Store/Play Store 표시명도 의도대로 반영되는지 확인 바랍니다.
🤖 Prompt for AI Agents
In app.json around line 3 update the app name to exactly "두둑" (remove the
English alias), then search and replace occurrences of '두둑(Doodook)' in the
codebase (e.g., src/screens/Auth/SignUp1Screen.js line 27) to '두둑' so the
display name is consistent; finally verify app store / Play Store displayName
settings (app.json or platform-specific config) reflect "두둑" as intended and run
a build to confirm the visible name.
| "extra": { | ||
| "eas": { | ||
| "projectId": "57b0a621-af5d-4605-b6e0-cc46a6c474ec" | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
EAS 프로젝트 ID 추가 확인
expo.extra.eas.projectId가 추가되었습니다. 이 프로젝트 ID가 올바른 EAS 프로젝트와 매핑되는지 확인하고, 관련 EAS 구성(eas.json)이 이 프로젝트 ID와 일치하는지 검증하세요.
🏁 Script executed:
#!/bin/bash
# EAS 구성 파일이 존재하고 프로젝트 ID가 일치하는지 확인
if [ -f "eas.json" ]; then
cat eas.json | jq -r '.cli.projectId'
else
echo "eas.json 파일을 찾을 수 없습니다"
fiLength of output: 4
eas.json 파일 내에 projectId 키가 설정되어 있는지 확인해 주세요. 아래 명령어를 실행하고 결과를 공유해 주세요.
🏁 Script executed:
#!/bin/bash
rg -n '"projectId"' -C2 eas.json || echo "projectId 키를 찾을 수 없습니다."Length of output: 23
EAS 구성 파일에 projectId 추가 필요
eas.json에 .cli.projectId가 설정되어 있지 않습니다. app.json에 지정된 프로젝트 ID(57b0a621-af5d-4605-b6e0-cc46a6c474ec)와 동일한 값을 eas.json의 .cli.projectId에 추가하세요.
🤖 Prompt for AI Agents
In app.json around lines 51 to 55 the EAS projectId is set to
57b0a621-af5d-4605-b6e0-cc46a6c474ec; add the same value to eas.json under the
.cli.projectId key (e.g. set ".cli": { "projectId":
"57b0a621-af5d-4605-b6e0-cc46a6c474ec" }) so eas.json and app.json use the
identical projectId.
| } catch (error) { | ||
| console.log("ℹ️ MBTI 추천 정보 가져오기 완료:", error.message || error); | ||
| } finally { | ||
| setAliasLoading(false); | ||
| } |
There was a problem hiding this comment.
에러 로깅 문구 수정.
catch에서 “완료”는 오해를 유발합니다. 에러로 명확히 표시하세요.
- } catch (error) {
- console.log("ℹ️ MBTI 추천 정보 가져오기 완료:", error.message || error);
+ } catch (error) {
+ console.warn("❌ MBTI 추천 정보 가져오기 오류:", error?.message || error);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| } catch (error) { | |
| console.log("ℹ️ MBTI 추천 정보 가져오기 완료:", error.message || error); | |
| } finally { | |
| setAliasLoading(false); | |
| } | |
| } catch (error) { | |
| console.warn("❌ MBTI 추천 정보 가져오기 오류:", error?.message || error); | |
| } finally { | |
| setAliasLoading(false); | |
| } |
🤖 Prompt for AI Agents
In src/screens/MyPage/MyPageScreen.js around lines 90 to 94, the catch block
logs "완료" which is misleading on errors; change the logging to clearly indicate
an error (e.g., use console.error and a message like "MBTI 추천 정보 가져오기 중 에러:"),
include the error object or error.message for full context, and keep the finally
block as-is to call setAliasLoading(false).
| try { | ||
| const msg = await increaseBalance(navigation, prize.value); | ||
| Alert.alert('🎉 축하합니다!', `${prize.amount} 당첨!\n\n${msg}`, [ | ||
| { text: '확인', style: 'default' } | ||
| ]); | ||
| } catch (error) { | ||
| Alert.alert( | ||
| '⏰ 오늘의 기회 소진', | ||
| '룰렛은 하루에 한 번만 도전 가능합니다.\n내일 다시 도전해보세요! 🍀', | ||
| [{ text: '확인', style: 'default' }] | ||
| ); | ||
| } finally { | ||
| setSpinning(false); | ||
| spinAnim.setValue(final); | ||
| } |
There was a problem hiding this comment.
에러 유형별 메시지 분기 필요
increaseBalance는 'already_used_today', 'network_error', 'parsing_error' 등으로 거절합니다. 현재 catch는 모든 경우를 “하루 한 번”으로 안내해 오도 소지가 있습니다. 에러 종류에 따라 안내를 분기하세요.
- } catch (error) {
- Alert.alert(
- '⏰ 오늘의 기회 소진',
- '룰렛은 하루에 한 번만 도전 가능합니다.\n내일 다시 도전해보세요! 🍀',
- [{ text: '확인', style: 'default' }]
- );
+ } catch (error) {
+ if (error === 'already_used_today') {
+ Alert.alert(
+ '⏰ 오늘의 기회 소진',
+ '룰렛은 하루에 한 번만 도전 가능합니다.\n내일 다시 도전해보세요! 🍀',
+ [{ text: '확인', style: 'default' }]
+ );
+ } else if (error === 'network_error') {
+ Alert.alert('네트워크 오류', '네트워크 상태를 확인하고 다시 시도해주세요.', [
+ { text: '확인', style: 'default' },
+ ]);
+ } else {
+ Alert.alert('오류', '잠시 후 다시 시도해주세요.', [
+ { text: '확인', style: 'default' },
+ ]);
+ }
} finally {
setSpinning(false);
spinAnim.setValue(final);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| const msg = await increaseBalance(navigation, prize.value); | |
| Alert.alert('🎉 축하합니다!', `${prize.amount} 당첨!\n\n${msg}`, [ | |
| { text: '확인', style: 'default' } | |
| ]); | |
| } catch (error) { | |
| Alert.alert( | |
| '⏰ 오늘의 기회 소진', | |
| '룰렛은 하루에 한 번만 도전 가능합니다.\n내일 다시 도전해보세요! 🍀', | |
| [{ text: '확인', style: 'default' }] | |
| ); | |
| } finally { | |
| setSpinning(false); | |
| spinAnim.setValue(final); | |
| } | |
| try { | |
| const msg = await increaseBalance(navigation, prize.value); | |
| Alert.alert('🎉 축하합니다!', `${prize.amount} 당첨!\n\n${msg}`, [ | |
| { text: '확인', style: 'default' } | |
| ]); | |
| } catch (error) { | |
| if (error === 'already_used_today') { | |
| Alert.alert( | |
| '⏰ 오늘의 기회 소진', | |
| '룰렛은 하루에 한 번만 도전 가능합니다.\n내일 다시 도전해보세요! 🍀', | |
| [{ text: '확인', style: 'default' }] | |
| ); | |
| } else if (error === 'network_error') { | |
| Alert.alert('네트워크 오류', '네트워크 상태를 확인하고 다시 시도해주세요.', [ | |
| { text: '확인', style: 'default' }, | |
| ]); | |
| } else { | |
| Alert.alert('오류', '잠시 후 다시 시도해주세요.', [ | |
| { text: '확인', style: 'default' }, | |
| ]); | |
| } | |
| } finally { | |
| setSpinning(false); | |
| spinAnim.setValue(final); | |
| } |
🤖 Prompt for AI Agents
In src/screens/MyPage/RouletteScreen.js around lines 117 to 131, the catch block
treats all failures as “already used today”; change it to inspect the error
(e.g., error.code or error.type) and branch messages: if code ===
'already_used_today' show the existing "오늘의 기회 소진" alert; if code ===
'network_error' show a network-related retry message; if code ===
'parsing_error' show a message asking to try again later or contact support;
otherwise show a generic error alert with the error message. Keep the finally
block behavior (setSpinning(false) and spinAnim.setValue(final)) unchanged.
| // ======================================== | ||
| // src/utils/theme.js | ||
| // ======================================== | ||
|
|
||
| export const themes = { | ||
| default: { | ||
| background: { | ||
| primary: '#003340', | ||
| secondary: '#004455', | ||
| card: 'rgba(255, 255, 255, 0.09)', | ||
| }, | ||
| text: { | ||
| primary: '#EFF1F5', | ||
| secondary: '#B8C5D1', | ||
| tertiary: '#6B7280', | ||
| disabled: '#AAAAAA', | ||
| }, | ||
| accent: { | ||
| primary: '#F074BA', | ||
| light: '#FFD1EB', | ||
| pale: '#fb9dd2ff', | ||
| }, | ||
| status: { | ||
| up: '#F074BA', | ||
| down: '#00BFFF', | ||
| same: '#AAAAAA', | ||
| success: '#6EE69E', | ||
| error: '#FF6B6B', | ||
| }, | ||
| chart: { | ||
| colors: [ | ||
| '#F074BA', '#3B82F6', '#34D399', '#10B981', | ||
| '#F59E0B', '#EF4444', '#6366F1', '#8B5CF6', | ||
| '#EC4899', '#F87171', '#FBBF24', '#4ADE80', | ||
| '#22D3EE', '#60A5FA', '#A78BFA', '#F472B6', | ||
| ], | ||
| }, | ||
| button: { | ||
| primary: '#F074BA', | ||
| secondary: '#EFF1F5', | ||
| info: '#6366F1', | ||
| }, | ||
| border: { | ||
| light: 'rgba(255, 255, 255, 0.08)', | ||
| medium: 'rgba(255, 255, 255, 0.1)', | ||
| }, | ||
| }, | ||
|
|
||
| stack: { | ||
| background: { | ||
| primary: '#F8FAF5', | ||
| secondary: '#E8EFE5', | ||
| card: 'rgba(44, 173, 102, 0.08)', | ||
| }, | ||
| text: { | ||
| primary: '#1A2E1A', | ||
| secondary: '#2D5A3D', | ||
| tertiary: '#6B8E70', | ||
| disabled: '#A8B5A8', | ||
| }, | ||
| accent: { | ||
| primary: '#2CAD66', | ||
| light: '#7FD99A', | ||
| pale: '#A8E6C1', | ||
| }, | ||
| status: { | ||
| up: '#2CAD66', | ||
| down: '#FF8C42', | ||
| same: '#8E9E8E', | ||
| success: '#2CAD66', | ||
| error: '#E85D4A', | ||
| }, | ||
| chart: { | ||
| colors: [ | ||
| '#2CAD66', '#4ECDC4', '#667EEA', '#95E1D3', | ||
| '#7B68EE', '#3498DB', '#FFB84D', '#FF8C94', | ||
| '#9B59B6', '#1ABC9C', '#5DADE2', '#AF7AC5', | ||
| '#52C1B8', '#85C1E2', '#B19CD9', '#6C9A8B', | ||
| ], | ||
| }, | ||
| button: { | ||
| primary: '#2CAD66', | ||
| secondary: '#4ECDC4', | ||
| info: '#667EEA', | ||
| }, | ||
| border: { | ||
| light: 'rgba(44, 173, 102, 0.12)', | ||
| medium: 'rgba(44, 173, 102, 0.2)', | ||
| }, | ||
| }, | ||
|
|
||
| premium: { | ||
| background: { | ||
| primary: '#0A1929', | ||
| secondary: '#132F4C', | ||
| card: 'rgba(255, 215, 0, 0.05)', | ||
| }, | ||
| text: { | ||
| primary: '#F0F3F7', | ||
| secondary: '#C2CDD9', | ||
| tertiary: '#8B99A8', | ||
| disabled: '#647586', | ||
| }, | ||
| accent: { | ||
| primary: '#FFD700', | ||
| light: '#FFE97F', | ||
| pale: '#FFF4CC', | ||
| }, | ||
| status: { | ||
| up: '#FFD700', | ||
| down: '#6495ED', | ||
| same: '#90A4AE', | ||
| success: '#4FC3F7', | ||
| error: '#FF7961', | ||
| }, | ||
| chart: { | ||
| colors: [ | ||
| '#FFD700', '#6495ED', '#4FC3F7', '#BA68C8', | ||
| '#FF8A65', '#4DD0E1', '#AED581', '#FFB74D', | ||
| '#9575CD', '#4DB6AC', '#F06292', '#7986CB', | ||
| '#64B5F6', '#81C784', '#FFD54F', '#A1887F', | ||
| ], | ||
| }, | ||
| button: { | ||
| primary: '#FFD700', | ||
| secondary: '#6495ED', | ||
| info: '#4FC3F7', | ||
| }, | ||
| border: { | ||
| light: 'rgba(255, 255, 255, 0.08)', | ||
| medium: 'rgba(255, 255, 255, 0.12)', | ||
| }, | ||
| }, | ||
|
|
||
| sakura: { | ||
| background: { | ||
| primary: '#FFF5F7', | ||
| secondary: '#FFE4E9', | ||
| card: 'rgba(255, 192, 203, 0.08)', | ||
| }, | ||
| text: { | ||
| primary: '#2D1F2E', | ||
| secondary: '#5A4A5E', | ||
| tertiary: '#8B7A8F', | ||
| disabled: '#B8ADB9', | ||
| }, | ||
| accent: { | ||
| primary: '#FFB7C5', | ||
| light: '#FFD4DC', | ||
| pale: '#FFEAF0', | ||
| }, | ||
| status: { | ||
| up: '#98D8C8', | ||
| down: '#F7A4BC', | ||
| same: '#C5B8C9', | ||
| success: '#7EC4B6', | ||
| error: '#E88D99', | ||
| }, | ||
| chart: { | ||
| colors: [ | ||
| '#FFB7C5', '#98D8C8', '#C5A3D9', '#A8D8EA', | ||
| '#FFCAD4', '#B4E7CE', '#E8BBE0', '#87CEEB', | ||
| '#F7A4BC', '#7EC4B6', '#D4A5C7', '#9BD3D0', | ||
| '#FFC9D4', '#A5D8CF', '#E0B8D3', '#7FD5D5', | ||
| ], | ||
| }, | ||
| button: { | ||
| primary: '#FFB7C5', | ||
| secondary: '#98D8C8', | ||
| info: '#C5A3D9', | ||
| }, | ||
| border: { | ||
| light: 'rgba(255, 183, 197, 0.15)', | ||
| medium: 'rgba(255, 183, 197, 0.25)', | ||
| }, | ||
| }, | ||
|
|
||
| ocean: { | ||
| background: { | ||
| primary: '#001C30', | ||
| secondary: '#00324A', | ||
| card: 'rgba(0, 212, 255, 0.08)', | ||
| }, | ||
| text: { | ||
| primary: '#E3F4FF', | ||
| secondary: '#B3DAF0', | ||
| tertiary: '#7FAEC8', | ||
| disabled: '#5A8CAF', | ||
| }, | ||
| accent: { | ||
| primary: '#00D4FF', | ||
| light: '#66E4FF', | ||
| pale: '#B3F0FF', | ||
| }, | ||
| status: { | ||
| up: '#4DFFA6', | ||
| down: '#FF6B9D', | ||
| same: '#7BA3B8', | ||
| success: '#26E7A6', | ||
| error: '#FF5C7C', | ||
| }, | ||
| chart: { | ||
| colors: [ | ||
| '#00D4FF', '#4DFFA6', '#FF6B9D', '#B794F4', | ||
| '#38B6FF', '#5EE3C1', '#FF8FB1', '#9D7EF0', | ||
| '#0099CC', '#4ECDC4', '#FF85A2', '#8B7FC7', | ||
| '#26C6DA', '#7FD99A', '#FFA0BA', '#A78BFA', | ||
| ], | ||
| }, | ||
| button: { | ||
| primary: '#00D4FF', | ||
| secondary: '#4DFFA6', | ||
| info: '#B794F4', | ||
| }, | ||
| border: { | ||
| light: 'rgba(0, 212, 255, 0.12)', | ||
| medium: 'rgba(0, 212, 255, 0.2)', | ||
| }, | ||
| }, | ||
|
|
||
| autumn: { | ||
| background: { | ||
| primary: '#2C1810', | ||
| secondary: '#3E2418', | ||
| card: 'rgba(218, 136, 72, 0.08)', | ||
| }, | ||
| text: { | ||
| primary: '#FFF5E6', | ||
| secondary: '#E8D2B8', | ||
| tertiary: '#C4A885', | ||
| disabled: '#9E8870', | ||
| }, | ||
| accent: { | ||
| primary: '#DA8848', | ||
| light: '#F4B17A', | ||
| pale: '#FFD6A5', | ||
| }, | ||
| status: { | ||
| up: '#E8A24E', | ||
| down: '#9B6B4D', | ||
| same: '#A89080', | ||
| success: '#C69F73', | ||
| error: '#D64545', | ||
| }, | ||
| chart: { | ||
| colors: [ | ||
| '#DA8848', '#B4846C', '#E8A24E', '#8B6F47', | ||
| '#F4B17A', '#A67B5B', '#FFB366', '#9E7C5A', | ||
| '#E09F3E', '#8B7355', '#FFD085', '#A28A6B', | ||
| '#D4A574', '#7A6148', '#FFC872', '#B8956A', | ||
| ], | ||
| }, | ||
| button: { | ||
| primary: '#DA8848', | ||
| secondary: '#E8A24E', | ||
| info: '#C69F73', | ||
| }, | ||
| border: { | ||
| light: 'rgba(218, 136, 72, 0.15)', | ||
| medium: 'rgba(218, 136, 72, 0.25)', | ||
| }, | ||
| }, | ||
|
|
||
| midnight: { | ||
| background: { | ||
| primary: '#1A0F2E', | ||
| secondary: '#2D1B4E', | ||
| card: 'rgba(147, 112, 219, 0.08)', | ||
| }, | ||
| text: { | ||
| primary: '#F0E6FF', | ||
| secondary: '#C9B8E4', | ||
| tertiary: '#9B86BD', | ||
| disabled: '#7A6B94', | ||
| }, | ||
| accent: { | ||
| primary: '#9370DB', | ||
| light: '#B8A4E0', | ||
| pale: '#E0D5F7', | ||
| }, | ||
| status: { | ||
| up: '#A78BFA', | ||
| down: '#60A5FA', | ||
| same: '#8B7FA8', | ||
| success: '#818CF8', | ||
| error: '#F472B6', | ||
| }, | ||
| chart: { | ||
| colors: [ | ||
| '#9370DB', '#60A5FA', '#F472B6', '#34D399', | ||
| '#8B5CF6', '#3B82F6', '#EC4899', '#10B981', | ||
| '#A78BFA', '#60A5FA', '#F87171', '#14B8A6', | ||
| '#C084FC', '#93C5FD', '#FDA4AF', '#6EE7B7', | ||
| ], | ||
| }, | ||
| button: { | ||
| primary: '#9370DB', | ||
| secondary: '#60A5FA', | ||
| info: '#818CF8', | ||
| }, | ||
| border: { | ||
| light: 'rgba(147, 112, 219, 0.12)', | ||
| medium: 'rgba(147, 112, 219, 0.2)', | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| export const defaultTheme = themes.default; No newline at end of file |
There was a problem hiding this comment.
테마 정의가 화면 파일에 중복 포함됨 — 분리 필요(블로커)
Screen 파일에 themes/defaultTheme 전체가 재정의되어 있습니다. 단일 소스(src/utils/theme.js)로 유지하지 않으면 불일치·번들 비대화가 발생합니다.
- // ========================================
- // src/utils/theme.js
- // ========================================
-
- export const themes = {
- ... 수백 줄 ...
- };
-
- export const defaultTheme = themes.default;대신 ThemeContext에서 src/utils/theme.js의 테마를 import/주입해 사용하세요. 이 파일에서는 테마 데이터 정의를 제거하십시오. (필요 시 useTheme()에서 제공하는 값만 사용)
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/screens/MyPage/ThemeSelectorScreen.js around lines 391 to 698, the full
themes/defaultTheme definition is duplicated inside this screen — remove the
entire theme object block from this file and instead import the canonical themes
(and defaultTheme) from src/utils/theme.js; update the file to use the
ThemeContext or useTheme() values (or the imported theme constants) wherever the
local theme object was referenced so the screen relies on the single source of
truth and avoids bundling/consistency issues.
| console.log("📢 [Push] ExpoPushToken:", expoPushToken); | ||
|
|
There was a problem hiding this comment.
Expo Push 토큰 전체 로깅은 피하세요 (민감정보 노출 위험).
토큰은 식별자/자원으로 간주됩니다. 전체 값을 콘솔에 남기지 말고 DEV 빌드에서만 마스킹해 출력하세요.
- console.log("📢 [Push] ExpoPushToken:", expoPushToken);
+ if (__DEV__) {
+ console.log("📢 [Push] ExpoPushToken:", (expoPushToken ?? "").slice(0, 16) + "…");
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| console.log("📢 [Push] ExpoPushToken:", expoPushToken); | |
| if (__DEV__) { | |
| console.log("📢 [Push] ExpoPushToken:", (expoPushToken ?? "").slice(0, 16) + "…"); | |
| } |
🤖 Prompt for AI Agents
In src/services/PushNotificationService.ios.js around lines 136-137, avoid
logging the full Expo push token; instead only log a masked version in
development and never log it in production. Replace the raw console.log with a
conditional that runs only in DEV (e.g. __DEV__ or NODE_ENV check) and log a
safely masked token (showing only a small prefix/suffix or last 4 chars with the
rest replaced by asterisks) along with contextual text; ensure production builds
emit no token value at all.
| export const themes = { | ||
| // ✅ 기본 테마 (유지) | ||
| default: { | ||
| background: { | ||
| primary: '#003340', | ||
| secondary: '#004455', | ||
| card: 'rgba(255, 255, 255, 0.09)', | ||
| }, | ||
| text: { | ||
| primary: '#EFF1F5', | ||
| secondary: '#B8C5D1', | ||
| tertiary: '#6B7280', | ||
| disabled: '#AAAAAA', | ||
| }, | ||
| accent: { | ||
| primary: '#F074BA', | ||
| light: '#FFD1EB', | ||
| pale: '#fb9dd2ff', | ||
| }, | ||
| status: { | ||
| up: '#F074BA', | ||
| down: '#00BFFF', | ||
| same: '#AAAAAA', | ||
| success: '#6EE69E', | ||
| error: '#FF6B6B', | ||
| }, | ||
| chart: { | ||
| colors: [ | ||
| '#F074BA', '#3B82F6', '#34D399', '#10B981', | ||
| '#F59E0B', '#EF4444', '#6366F1', '#8B5CF6', | ||
| '#EC4899', '#F87171', '#FBBF24', '#4ADE80', | ||
| '#22D3EE', '#60A5FA', '#A78BFA', '#F472B6', | ||
| ], | ||
| }, | ||
| button: { | ||
| primary: '#F074BA', | ||
| secondary: '#EFF1F5', | ||
| info: '#6366F1', | ||
| }, | ||
| border: { | ||
| light: 'rgba(255, 255, 255, 0.08)', | ||
| medium: 'rgba(255, 255, 255, 0.1)', | ||
| }, | ||
| }, | ||
|
|
||
| // 🌿 stack - 민트 화이트 | ||
| stack: { | ||
| background: { | ||
| primary: '#F9FCF9', | ||
| secondary: '#EAF4ED', | ||
| card: 'rgba(44, 173, 102, 0.06)', | ||
| }, | ||
| text: { | ||
| primary: '#1F3A28', | ||
| secondary: '#3F5A46', | ||
| tertiary: '#78937B', | ||
| disabled: '#A8B8A8', | ||
| }, | ||
| accent: { | ||
| primary: '#2CAD66', | ||
| light: '#A8E6C1', | ||
| pale: '#DFF9E9', | ||
| }, | ||
| status: { | ||
| up: '#A8E6C1', | ||
| down: '#FFCA61', | ||
| same: '#94A59B', | ||
| success: '#4ADE80', | ||
| error: '#E85D4A', | ||
| }, | ||
| chart: { | ||
| colors: [ | ||
| '#2CAD66', | ||
| '#FFD166', | ||
| '#6EE69E', | ||
| '#FF8FB1', | ||
| '#26C6DA', | ||
| '#B39DDB', | ||
| '#7FD99A', | ||
| '#E85D4A', | ||
| '#81C784', | ||
| '#F4E285', | ||
| '#A78BD4', | ||
| '#4FC3F7', | ||
| '#FFB74D', | ||
| '#C8E6C9', | ||
| '#F8BBD0', | ||
| '#CFD8DC', | ||
| ] | ||
|
|
||
| }, | ||
| button: { | ||
| primary: '#2CAD66', | ||
| secondary: '#C7EFD4', | ||
| info: '#4ECDC4', | ||
| }, | ||
| border: { | ||
| light: 'rgba(44, 173, 102, 0.12)', | ||
| medium: 'rgba(44, 173, 102, 0.22)', | ||
| }, | ||
| }, | ||
|
|
||
| // ✨ premium - 화이트 & 골드 | ||
| premium: { | ||
| background: { | ||
| primary: '#FDFBF7', | ||
| secondary: '#F5F1E6', | ||
| card: 'rgba(255, 215, 0, 0.07)', | ||
| }, | ||
| text: { | ||
| primary: '#3A3A3A', | ||
| secondary: '#6B6B6B', | ||
| tertiary: '#9B9B9B', | ||
| disabled: '#C0C0C0', | ||
| }, | ||
| accent: { | ||
| primary: '#C6A200', | ||
| light: '#FFE97F', | ||
| pale: '#FFF5CC', | ||
| }, | ||
| status: { | ||
| up: '#C6A200', | ||
| down: '#6BB1FF', | ||
| same: '#A8A8A8', | ||
| success: '#7FD4C2', | ||
| error: '#FF7C7C', | ||
| }, | ||
| chart: { | ||
| colors: [ | ||
| '#C6A200', '#FFE97F', '#FFB84D', '#BFA181', | ||
| '#4FC3F7', '#FFD54F', '#8BC34A', '#FDD835', | ||
| '#BA68C8', '#FF8A65', '#FFD740', '#AED581', | ||
| '#4DD0E1', '#FFF9C4', '#FFB300', '#FFE082', | ||
| ], | ||
| }, | ||
| button: { | ||
| primary: '#C6A200', | ||
| secondary: '#F7E8B3', | ||
| info: '#BFA181', | ||
| }, | ||
| border: { | ||
| light: 'rgba(198, 162, 0, 0.15)', | ||
| medium: 'rgba(198, 162, 0, 0.25)', | ||
| }, | ||
| }, | ||
|
|
||
| // 🌸 sakura (유지) | ||
| sakura: { | ||
| background: { | ||
| primary: '#FFF5F7', | ||
| secondary: '#FFE4E9', | ||
| card: 'rgba(255, 192, 203, 0.08)', | ||
| }, | ||
| text: { | ||
| primary: '#2D1F2E', | ||
| secondary: '#5A4A5E', | ||
| tertiary: '#8B7A8F', | ||
| disabled: '#B8ADB9', | ||
| }, | ||
| accent: { | ||
| primary: '#FFB7C5', | ||
| light: '#FFD4DC', | ||
| pale: '#FFEAF0', | ||
| }, | ||
| status: { | ||
| up: '#98D8C8', | ||
| down: '#F7A4BC', | ||
| same: '#C5B8C9', | ||
| success: '#7EC4B6', | ||
| error: '#E88D99', | ||
| }, | ||
| chart: { | ||
| colors: [ | ||
| '#FFB7C5', '#98D8C8', '#A8D8EA', '#F7A4BC', | ||
| '#C5A3D9', '#E0B8D3', '#FFC9D4', '#B4E7CE', | ||
| '#E8BBE0', '#87CEEB', '#9BD3D0', '#FFD1FF', | ||
| '#A5D8CF', '#FAD0C4', '#E0B8D3', '#7FD5D5', | ||
| ], | ||
| }, | ||
| button: { | ||
| primary: '#FFB7C5', | ||
| secondary: '#98D8C8', | ||
| info: '#C5A3D9', | ||
| }, | ||
| border: { | ||
| light: 'rgba(255, 183, 197, 0.15)', | ||
| medium: 'rgba(255, 183, 197, 0.25)', | ||
| }, | ||
| }, | ||
|
|
||
| // 🩵 ocean - 스카이블루 & 화이트 | ||
| ocean: { | ||
| background: { | ||
| primary: '#F4FBFF', | ||
| secondary: '#E5F6FD', | ||
| card: 'rgba(0, 168, 232, 0.07)', | ||
| }, | ||
| text: { | ||
| primary: '#0F2A3A', | ||
| secondary: '#355870', | ||
| tertiary: '#7195A8', | ||
| disabled: '#A5B7C2', | ||
| }, | ||
| accent: { | ||
| primary: '#00A8E8', | ||
| light: '#7FD4F9', | ||
| pale: '#D9F5FF', | ||
| }, | ||
| status: { | ||
| up: '#00C6A8', | ||
| down: '#FF8FB1', | ||
| same: '#9DBCC6', | ||
| success: '#34D399', | ||
| error: '#FF6B81', | ||
| }, | ||
| chart: { | ||
| colors: [ | ||
| '#00A8E8', '#34D399', '#7FD4F9', '#FFB84D', | ||
| '#5EE3C1', '#F59E0B', '#6EE7B7', '#9D7EF0', | ||
| '#22D3EE', '#FF85A2', '#1ABC9C', '#60A5FA', | ||
| '#81C784', '#A78BFA', '#4ECDC4', '#64B5F6', | ||
| ], | ||
| }, | ||
| button: { | ||
| primary: '#00A8E8', | ||
| secondary: '#9EE3FA', | ||
| info: '#6BB3E5', | ||
| }, | ||
| border: { | ||
| light: 'rgba(0, 168, 232, 0.15)', | ||
| medium: 'rgba(0, 168, 232, 0.25)', | ||
| }, | ||
| }, | ||
|
|
||
| // 🍂 autumn - 코랄 베이지 | ||
| autumn: { | ||
| background: { | ||
| primary: '#FFF9F4', | ||
| secondary: '#FDEFE4', | ||
| card: 'rgba(218, 136, 72, 0.07)', | ||
| }, | ||
| text: { | ||
| primary: '#3A251A', | ||
| secondary: '#6B4E3A', | ||
| tertiary: '#A0765B', | ||
| disabled: '#BFA592', | ||
| }, | ||
| accent: { | ||
| primary: '#E7985A', | ||
| light: '#FBCB9E', | ||
| pale: '#FFE8D0', | ||
| }, | ||
| status: { | ||
| up: '#F8B878', | ||
| down: '#C6794B', | ||
| same: '#A08D7B', | ||
| success: '#E8A24E', | ||
| error: '#D64545', | ||
| }, | ||
| chart: { | ||
| colors: [ | ||
| '#E7985A', '#FBCB9E', '#C6794B', '#FFD085', | ||
| '#D4A574', '#F8B878', '#FFB366', '#FFC872', | ||
| '#E09F3E', '#F7D4B2', '#FFCD94', '#EFC97B', | ||
| '#FFB84D', '#F4B17A', '#DDA56B', '#FDD5A5', | ||
| ], | ||
| }, | ||
| button: { | ||
| primary: '#E7985A', | ||
| secondary: '#F7D4B2', | ||
| info: '#DDA56B', | ||
| }, | ||
| border: { | ||
| light: 'rgba(231, 152, 90, 0.15)', | ||
| medium: 'rgba(231, 152, 90, 0.25)', | ||
| }, | ||
| }, | ||
|
|
||
| // 🌙 midnight - 라벤더 그레이 | ||
| midnight: { | ||
| background: { | ||
| primary: '#F8F6FB', | ||
| secondary: '#EDE9F7', | ||
| card: 'rgba(147, 112, 219, 0.07)', | ||
| }, | ||
| text: { | ||
| primary: '#2E254A', | ||
| secondary: '#5A4E7A', | ||
| tertiary: '#8A7FA5', | ||
| disabled: '#B8B0C9', | ||
| }, | ||
| accent: { | ||
| primary: '#9370DB', | ||
| light: '#C3B1F2', | ||
| pale: '#E9E1FF', | ||
| }, | ||
| status: { | ||
| up: '#B09EFF', | ||
| down: '#F6A6E0', | ||
| same: '#A8A0C0', | ||
| success: '#A78BFA', | ||
| error: '#F472B6', | ||
| }, | ||
| chart: { | ||
| colors: [ | ||
| '#9370DB', '#A78BFA', '#F472B6', '#C3B1F2', | ||
| '#60A5FA', '#8B5CF6', '#E9E1FF', '#9B86BD', | ||
| '#C084FC', '#FDA4AF', '#818CF8', '#D4BFFF', | ||
| '#CDB5FF', '#E2C9FA', '#F5B9E7', '#A6A2FF', | ||
| ], | ||
| }, | ||
| button: { | ||
| primary: '#9370DB', | ||
| secondary: '#C3B1F2', | ||
| info: '#B09EFF', | ||
| }, | ||
| border: { | ||
| light: 'rgba(147, 112, 219, 0.15)', | ||
| medium: 'rgba(147, 112, 219, 0.25)', | ||
| }, | ||
| }, | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
테마 정의 중복 제거 필요
이 파일에 정의된 테마 데이터가 src/screens/MyPage/ThemeSelectorScreen.js 파일(394-697라인)에도 동일하게 중복되어 있습니다. 이는 DRY 원칙을 위반하며, 유지보수 시 두 위치를 모두 업데이트해야 하는 문제를 야기합니다.
ThemeSelectorScreen.js가 이 파일에서 themes를 임포트하여 사용하도록 리팩토링하세요.
#!/bin/bash
# ThemeSelectorScreen.js에서 themes 정의가 중복되는지 확인
ast-grep --pattern $'export const themes = {
$$$
}'🤖 Prompt for AI Agents
In src/utils/theme.js (lines 3-324) and
src/screens/MyPage/ThemeSelectorScreen.js (lines 394-697), the same themes
object is duplicated; remove the duplicate definition in ThemeSelectorScreen.js
and import the shared export from src/utils/theme.js instead. Specifically:
delete the local export const themes block in ThemeSelectorScreen.js, add an
import for the named export themes from src/utils/theme.js using the correct
relative path, update any local references to use the imported themes, and run
lint/type checks and app to confirm no import path or name mismatches remain.
📋 개요
변경 사항 및 관련 이슈에 대해 간단하게 작성해주세요.
관련 이슈:
🏷️ PR 유형
어떤 변경 사항이 있나요?
📱 기능 관련
🎨 UI/UX 관련
🔧 개발 환경 관련
📚 문서화
🗂️ 구조 변경
📸 스크린샷 및 데모
변경된 UI나 새로운 기능이 있다면 스크린샷을 첨부해주세요.
Before (변경 전)
After (변경 후)
📱 테스트 환경
📝 상세 변경 내용
구체적인 변경 사항을 설명해주세요.
추가된 내용
수정된 내용
삭제된 내용
🔗 관련 파일
주요 변경된 파일들을 나열해주세요.
src/screens/src/components/src/services/src/utils/src/types/🚧 추가 작업 계획
이 PR 이후에 추가로 진행할 작업이 있다면 작성해주세요.
현재 제한사항
향후 개선 계획
후속 작업
💬 리뷰어에게
리뷰어가 특별히 주의 깊게 봐야 할 부분이나 질문이 있다면 작성해주세요.
📚 참고 자료
관련 문서, 디자인, 또는 참고한 자료가 있다면 링크를 첨부해주세요.
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선 사항