Skip to content

Kyh#1

Open
spacefoul wants to merge 9 commits intomainfrom
kyh
Open

Kyh#1
spacefoul wants to merge 9 commits intomainfrom
kyh

Conversation

@spacefoul
Copy link
Collaborator

@spacefoul spacefoul commented Oct 16, 2025

📋 개요

변경 사항 및 관련 이슈에 대해 간단하게 작성해주세요.

관련 이슈:

  • Resolves: #(Issue Number)

🏷️ PR 유형

어떤 변경 사항이 있나요?

📱 기능 관련

  • 새로운 기능 추가 (투자 시뮬레이션, 학습 콘텐츠 등)
  • 기존 기능 개선 및 수정
  • 버그 수정

🎨 UI/UX 관련

  • 사용자 인터페이스 디자인 변경
  • 사용자 경험(UX) 개선
  • 반응형 디자인 수정

🔧 개발 환경 관련

  • 코드 리팩토링
  • 성능 최적화
  • 코드에 영향을 주지 않는 변경사항 (오타 수정, 포맷팅 등)
  • 테스트 추가 및 수정
  • 빌드 설정 또는 패키지 의존성 변경

📚 문서화

  • 주석 추가 및 수정
  • README 또는 문서 수정
  • API 문서 업데이트

🗂️ 구조 변경

  • 파일 또는 폴더명 수정
  • 파일 또는 폴더 삭제
  • 프로젝트 구조 변경

📸 스크린샷 및 데모

변경된 UI나 새로운 기능이 있다면 스크린샷을 첨부해주세요.

Before (변경 전)

After (변경 후)

📱 테스트 환경

  • iOS Simulator
  • Android Emulator

📝 상세 변경 내용

구체적인 변경 사항을 설명해주세요.

추가된 내용

수정된 내용

삭제된 내용


🔗 관련 파일

주요 변경된 파일들을 나열해주세요.

  • src/screens/
  • src/components/
  • src/services/
  • src/utils/
  • src/types/
  • 기타:

🚧 추가 작업 계획

이 PR 이후에 추가로 진행할 작업이 있다면 작성해주세요.

현재 제한사항

향후 개선 계획

후속 작업


💬 리뷰어에게

리뷰어가 특별히 주의 깊게 봐야 할 부분이나 질문이 있다면 작성해주세요.


📚 참고 자료

관련 문서, 디자인, 또는 참고한 자료가 있다면 링크를 첨부해주세요.

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 테마 선택 기능 추가 - 다양한 디자인 테마 중 선택 가능
    • 푸시 알림 기능 강화 - 알림 수신 및 응답 처리 개선
    • 챗봇 화면 개선 - 메시지 제안 및 향상된 입력 환경 제공
  • 개선 사항

    • 키보드 감지 및 레이아웃 최적화로 입력 편의성 향상
    • 전체 화면에 일관된 테마 적용
    • 앱 이름 업데이트 (Doodook → 두둑)

@coderabbitai
Copy link

coderabbitai bot commented Oct 16, 2025

Walkthrough

테마 시스템을 새로 도입하고 30개 이상의 화면 컴포넌트에 동적 테마 지원을 추가했습니다. 동시에 푸시 알림 서비스를 재작성하고, 라우렛 화면을 업데이트하며, 네비게이션 구조와 앱 설정 파일을 변경했습니다.

Changes

코호트 / 파일 변경 요약
테마 시스템
src/utils/theme.js, src/utils/ThemeContext.js
7개 테마가 포함된 테마 정의 모듈 생성 및 테마 상태 관리, 로컬 저장소 연동, 테마 변경 제어 기능이 있는 ThemeContext 및 useTheme 훅 추가
인증 화면 테마 통합
src/screens/Auth/LoginScreen.js, src/screens/Auth/SignUp1Screen.js, src/screens/Auth/SignUp2Screen.js, src/screens/Auth/SignUp3Screen.js, src/screens/Auth/SignUp4Screen.js, src/screens/Auth/ResetPasswordScreen.js
모든 인증 화면에 useTheme 훅을 추가하고 하드코딩된 색상을 테마 토큰으로 대체
FindPasswordScreen 재설계
src/screens/Auth/FindPasswordScreen.js
2단계 비밀번호 찾기 플로우로 변경: 이메일 검증 → 인증 코드 및 새 비밀번호 입력, 키보드 인식 UI 추가
FindIdScreen 삭제
src/screens/Auth/FindIdScreen.js
파일 전체 삭제 및 네비게이션에서 제거
가이드 화면 테마 통합
src/screens/Guide/GuideScreen.js, src/screens/Guide/GuideLevel1.js, src/screens/Guide/GuideLevel2.js, src/screens/Guide/GuideLevel3.js, src/screens/Guide/StudyScreen.js, src/screens/Guide/TypeExamScreen.js, src/screens/Guide/TypeResultScreen.js
테마 지원 추가, 안전영역 인셋 처리, 에러 상태 및 재시도 로직 구현, 뮤직 아이콘 추가
메인 화면 테마 통합
src/screens/Main/MainScreen.js, src/screens/Main/SearchScreen.js, src/screens/Main/AssetDetailScreen.js, src/screens/Main/StockDetail.js, src/screens/Main/StockTradeScreen.js, src/screens/Main/TradingBuyScreen.js, src/screens/Main/TradingSellScreen.js
테마 색상 및 동적 스타일 적용, SVG 아이콘을 Lucide React Native 아이콘으로 교체
MyPage 화면 재설계
src/screens/MyPage/MyPageScreen.js, src/screens/MyPage/ChangePasswordScreen.js, src/screens/MyPage/FAQScreen.js, src/screens/MyPage/NoticeScreen.js, src/screens/MyPage/RouletteScreen.js, src/screens/MyPage/EditUserInfoScreen.js
테마 지원 추가, MBTI 추천 기능 추가, 신규 ThemeSelectorScreen 추가, 라우렛 UI 및 애니메이션 재구현, EditUserInfoScreen 비활성화
신규 ThemeSelectorScreen
src/screens/MyPage/ThemeSelectorScreen.js
테마 선택 UI 화면 추가, 테마 카드 그리드 및 즉시 적용 기능 포함
채봇 화면 전체 재구현
src/screens/Chatbot/ChatbotScreen.js
메시지 플로우, 타이핑 표시기, 키보드 인식 입력 바, 추천 제안 패널 추가
학습 진행률 바 테마 통합
src/components/LearningProgressBar.js
테마 기반 색상 적용
푸시 알림 서비스 재작성
src/services/PushNotificationService.ios.js
로컬 토큰 중심에서 서버 백업 중심으로 변경, API 업로드 기능 추가, 네비게이션 의존성 제거
네비게이션 업데이트
src/navigation/StackNavigator.js, src/navigation/MainTab.js
FindId 라우트 제거, ThemeSelector 라우트 추가, MainTab에 Lucide 아이콘 및 테마 적용
앱 진입점 수정
App.js
iOS 푸시 알림 초기화 경로 추가, 디바이스 확인, 권한 요청, 알림 리스너 등록 로직 추가
설정 파일 업데이트
.expo/devices.json, .expo/settings.json, app.config.js, app.json, package.json
디바이스 추적 정보 추가, Expo 설정 추가, app.config.js의 기본 내보내기 함수 제거, app.json에 소유자 및 프로젝트 ID 추가, 의존성 업데이트 및 스크립트 명령 변경
포인트 시스템 업데이트
src/utils/point.js
HTTP 오류 처리 방식 변경: 예외 던지기에서 Promise 거부로 전환

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: 테마 변경 완료
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

분석 근거:

  • 광범위한 영향: 30개 이상의 파일 수정, 거의 모든 화면 컴포넌트 영향
  • 이질적 변경: 테마 시스템 도입, 서비스 재작성, UI 재설계, 설정 변경 등 다양한 유형의 수정
  • 복잡한 로직 변경: FindPasswordScreen 2단계 플로우, ChatbotScreen 전체 재구현, RouletteScreen 애니메이션 재작성, PushNotificationService 아키텍처 변경 등 여러 고밀도 로직 수정
  • 반복성: 테마 적용은 반복적 패턴이지만 각 화면의 테마 포인트가 다르므로 개별 검토 필요
  • 중요한 시스템: 푸시 알림, 네비게이션, 핵심 서비스 등 중요한 부분 변경

Pre-merge checks and finishing touches

❌ Failed checks (3 warnings)
Check name Status Explanation Resolution
Title Check ⚠️ Warning PR 제목 “Kyh”는 변경된 주요 기능이나 수정 사항을 전혀 설명하지 않아 이 PR의 핵심 내용을 파악할 수 없습니다. 제목을 변경 사항의 핵심을 한 문장으로 명확하게 요약하는 형태로 수정해주세요. 예를 들어 “테마 시스템 통합 및 푸시 알림 초기화 로직 추가”와 같이 작성할 수 있습니다.
Description Check ⚠️ Warning PR 설명은 템플릿 구조만 남아 있고 실제 개요, 상세 변경 내용, 관련 이슈, 파일 목록, 스크린샷 등 핵심 정보가 모두 비어 있어 실질적인 설명이 제공되지 않습니다. 각 템플릿 섹션에 실제 변경된 내용과 관련 이슈 번호, 수정된 파일 목록, 스크린샷 또는 데모 영상을 포함하여 PR 설명을 완전하게 작성해주세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch kyh

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@spacefoul
Copy link
Collaborator Author

src 코드랑 App.js 코드 변경

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 90d54b2 and 5a9729d.

⛔ Files ignored due to path filters (9)
  • .expo/web/cache/production/images/iconsuniversal-icon/iconsuniversal-icon-b4d49e7392f44b0e1b4fd25a5b828b2bf3fd57e856a1c968620b71d61659e519-cover-#ffffff/App-Icon-1024x1024@1x.png is excluded by !**/*.png
  • .expo/web/cache/production/images/splash-ios/splash-ios-b4d49e7392f44b0e1b4fd25a5b828b2bf3fd57e856a1c968620b71d61659e519-contain/icon_undefined.png is excluded by !**/*.png
  • package-lock.json is excluded by !**/package-lock.json
  • src/assets/fonts/Pretendard-Bold.otf is excluded by !**/*.otf
  • src/assets/fonts/Pretendard-Medium.otf is excluded by !**/*.otf
  • src/assets/fonts/Pretendard-Regular.otf is excluded by !**/*.otf
  • src/assets/fonts/Pretendard-SemiBold.otf is excluded by !**/*.otf
  • src/assets/fonts/Pretendard-Thin.otf is excluded by !**/*.otf
  • src/assets/rainbow.png is 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)
  • email (30-30)
  • 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)
  • email (68-68)
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)
  • email (68-68)
  • 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: 변동 색상 테마 연동 LGTM

status 키 기반으로 안전한 폴백(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/은 올바른 URL https://...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: 데이터 로딩/네비/터치 처리 전반 LGTM

useFocusEffect 재로딩, 안전 영역 반영, 현재/완료/잠금 상태 렌더링이 명확합니다.

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" 스크린이 네비게이터에 실제로 등록되어 있는지 점검해주세요. 미등록 시 런타임 에러 발생합니다.

Comment on lines +2 to +7
"devices": [
{
"installationId": "F0B2F989-D042-4D9B-BE7C-8D09B00BD4C5",
"lastUsed": 1758012043849
}
]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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 파일을 찾을 수 없습니다"
fi

Length 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.

Comment on lines +1 to +8
{
"hostType": "lan",
"lanType": "ip",
"dev": true,
"minify": false,
"urlRandomness": null,
"https": false
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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 파일을 찾을 수 없습니다"
fi

Length 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.

Comment on lines +25 to +35
Notifications.setNotificationHandler({
handleNotification: async () => {
console.log("[Push] handleNotification called (foreground display enabled)");
return {
shouldShowBanner: true,
shouldShowList: true,
shouldPlaySound: true,
shouldSetBadge: true,
};
},
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
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": "두둑",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Comment on lines +51 to +55
"extra": {
"eas": {
"projectId": "57b0a621-af5d-4605-b6e0-cc46a6c474ec"
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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 파일을 찾을 수 없습니다"
fi

Length 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.

Comment on lines +90 to +94
} catch (error) {
console.log("ℹ️ MBTI 추천 정보 가져오기 완료:", error.message || error);
} finally {
setAliasLoading(false);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

에러 로깅 문구 수정.

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.

Suggested change
} 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).

Comment on lines +117 to +131
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);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

에러 유형별 메시지 분기 필요

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.

Suggested change
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.

Comment on lines +391 to +698
// ========================================
// 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

테마 정의가 화면 파일에 중복 포함됨 — 분리 필요(블로커)

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.

Comment on lines +136 to +137
console.log("📢 [Push] ExpoPushToken:", expoPushToken);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +3 to +324
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)',
},
},
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant