diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml index de89dad..97cf2b8 100644 --- a/.idea/caches/deviceStreaming.xml +++ b/.idea/caches/deviceStreaming.xml @@ -196,6 +196,18 @@ diff --git a/package-lock.json b/package-lock.json index 213dd2a..4c1478b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -93,6 +93,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -3502,6 +3503,7 @@ "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-6.1.18.tgz", "integrity": "sha512-mIT9MiL/vMm4eirLcmw2h6h/Nm5FICtnYSdohq4vTLA2FF/6PNhByM7s8ffqoVfE5L0uAa6Xda1B7oddolUiGg==", "license": "MIT", + "peer": true, "dependencies": { "@react-navigation/core": "^6.4.17", "escape-string-regexp": "^4.0.0", @@ -4316,6 +4318,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5328,6 +5331,7 @@ "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.29.tgz", "integrity": "sha512-9C90gyOzV83y2S3XzCbRDCuKYNaiyCzuP9ketv46acHCEZn+QTamPK/DobdghoSiofCmlfoaiD6/SzfxDiHMnw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "54.0.19", @@ -5430,6 +5434,7 @@ "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.10.tgz", "integrity": "sha512-UqyNaaLKRpj4pKAP4HZSLnuDQqueaO5tB1c/NWu5vh1/LF9ulItyyg2kF/IpeOp0DeOLk0GY0HrIXaKUMrwB+Q==", "license": "MIT", + "peer": true, "dependencies": { "fontfaceobserver": "^2.1.0" }, @@ -8975,6 +8980,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -9015,6 +9021,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -9045,6 +9052,7 @@ "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz", "integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==", "license": "MIT", + "peer": true, "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.81.5", @@ -9143,6 +9151,7 @@ "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz", "integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==", "license": "MIT", + "peer": true, "peerDependencies": { "react": "*", "react-native": "*" @@ -9153,6 +9162,7 @@ "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz", "integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==", "license": "MIT", + "peer": true, "dependencies": { "react-freeze": "^1.0.0", "react-native-is-edge-to-edge": "^1.2.1", @@ -9168,6 +9178,7 @@ "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.12.1.tgz", "integrity": "sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g==", "license": "MIT", + "peer": true, "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", @@ -9282,6 +9293,7 @@ "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.15.0.tgz", "integrity": "sha512-Vzjgy8mmxa/JO6l5KZrsTC7YemSdq+qB01diA0FqjUTaWGAGwuykpJ73MDj3+mzBSlaDxAEugHzTtkUQkQEQeQ==", "license": "MIT", + "peer": true, "dependencies": { "escape-string-regexp": "^4.0.0", "invariant": "2.2.4" diff --git a/src/components/modals/DeleteAccountModal.tsx b/src/components/modals/DeleteAccountModal.tsx index 88e39df..ce2ddeb 100644 --- a/src/components/modals/DeleteAccountModal.tsx +++ b/src/components/modals/DeleteAccountModal.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { View, Text, @@ -16,6 +16,8 @@ import { import { Ionicons as Icon } from "@expo/vector-icons"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { authAPI } from "../../services"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY } from "../../services/apiConfig"; interface DeleteAccountModalProps { isOpen: boolean; @@ -35,6 +37,7 @@ const DeleteAccountModal: React.FC = ({ const [showPassword, setShowPassword] = useState(false); const validateInputs = () => { + // 일반 사용자는 비밀번호와 탈퇴 사유 필수 if (!password.trim()) { Alert.alert("오류", "비밀번호를 입력해주세요."); return false; @@ -49,6 +52,7 @@ const DeleteAccountModal: React.FC = ({ }; const handleDeleteAccount = async () => { + // 입력 검증 if (!validateInputs()) { return; } @@ -68,6 +72,8 @@ const DeleteAccountModal: React.FC = ({ onPress: async () => { try { setLoading(true); + + // 일반 사용자 회원 탈퇴 const response = await authAPI.deleteAccount(password, reason); if (response.success) { diff --git a/src/navigation/types.ts b/src/navigation/types.ts index 969c4b0..d241805 100644 --- a/src/navigation/types.ts +++ b/src/navigation/types.ts @@ -29,7 +29,7 @@ export type RootStackParamList = { InBodyManual: undefined; HealthScoreTrend: undefined; // Payment - PaymentSuccess: { sessionId?: string; orderId?: string }; + PaymentSuccess: undefined; PaymentFail: undefined; PaymentCancel: undefined; }; diff --git a/src/screens/auth/KakaoOnboardingScreen.tsx b/src/screens/auth/KakaoOnboardingScreen.tsx index 854b9a5..b75f815 100644 --- a/src/screens/auth/KakaoOnboardingScreen.tsx +++ b/src/screens/auth/KakaoOnboardingScreen.tsx @@ -27,13 +27,14 @@ const healthGoalOptions = [ ]; const workoutDaysOptions = [ - {label: '주 1회', value: '주 1회'}, - {label: '주 2회', value: '주 2회'}, - {label: '주 3회', value: '주 3회'}, - {label: '주 4회', value: '주 4회'}, - {label: '주 5회', value: '주 5회'}, - {label: '주 6회', value: '주 6회'}, - {label: '주 7회', value: '주 7회'}, + {label: '주 1회', value: '1일'}, + {label: '주 2회', value: '2일'}, + {label: '주 3회', value: '3일'}, + {label: '주 3-4회', value: '3-4일'}, + {label: '주 4회', value: '4일'}, + {label: '주 5회', value: '5일'}, + {label: '주 6회', value: '6일'}, + {label: '주 7회', value: '7일'}, ]; const experienceLevelOptions = [ @@ -144,35 +145,53 @@ const KakaoOnboardingScreen = ({navigation}: any) => { try { const birthDate = `${formData.birthYear}-${String(formData.birthMonth).padStart(2, '0')}-${String(formData.birthDay).padStart(2, '0')}`; + // 유연성향상, 체력증진, 자세교정은 "MAINTENANCE"로 변환 + const healthGoalValue = ['FLEXIBILITY', 'ENDURANCE', 'POSTURE'].includes(formData.healthGoal) + ? 'MAINTENANCE' + : formData.healthGoal; + const onboardingData = { - gender: formData.gender, + birthDate, + agreePrivacy: true, + agreeTerms: true, + gender: formData.gender as "M" | "F", height: Number(formData.height), weight: Number(formData.weight), - birthDate, weightGoal: Number(formData.weightGoal), - healthGoal: formData.healthGoal, + healthGoal: healthGoalValue, workoutDaysPerWeek: formData.workoutDaysPerWeek, - ...(formData.experienceLevel && {experienceLevel: formData.experienceLevel}), - ...(formData.fitnessConcerns && {fitnessConcerns: formData.fitnessConcerns}), }; const response = await authAPI.submitOnboarding(onboardingData); - if (response.success) { - Alert.alert('완료', '신체정보가 저장되었습니다.', [ - { - text: '확인', - onPress: () => navigation.replace('Main'), - }, - ]); - } else { - Alert.alert('오류', response.message || '신체정보 저장에 실패했습니다.'); - } + setLoading(false); + + // 200 응답 (온보딩 완료) → Alert 없이 바로 홈으로 이동 + // response.success가 false여도 200 응답이면 성공으로 처리 + setTimeout(() => { + navigation.replace('Main'); + }, 100); + return; } catch (error: any) { console.error('온보딩 제출 실패:', error); - Alert.alert('오류', error.message || '신체정보 저장에 실패했습니다.'); - } finally { setLoading(false); + + // 400 에러 (이미 온보딩 완료됨) → Alert 없이 홈으로 이동 + if (error.status === 400) { + setTimeout(() => { + navigation.replace('Main'); + }, 100); + return; + } + + // 401 에러 (인증 필요) → 로그인 화면으로 이동 + if (error.status === 401) { + navigation.replace('Login'); + return; + } + + // 기타 에러만 Alert 표시 + Alert.alert('오류', error.message || '신체정보 저장에 실패했습니다.'); } }; diff --git a/src/screens/auth/LoginScreen.tsx b/src/screens/auth/LoginScreen.tsx index f6a4f77..97ee6ca 100644 --- a/src/screens/auth/LoginScreen.tsx +++ b/src/screens/auth/LoginScreen.tsx @@ -126,13 +126,17 @@ const LoginScreen = ({navigation}: any) => { const membershipType = (parsed.queryParams?.membershipType as string | undefined) || 'FREE'; await AsyncStorage.setItem('membershipType', membershipType); - // 온보딩 여부 확인 + // 온보딩 여부 확인 (isOnboarded 우선 확인) const isOnboarded = parsed.queryParams?.isOnboarded; const onboarded = parsed.queryParams?.onboarded; const shouldOnboard = isOnboarded === 'false' || onboarded === 'false'; + // 신규 유저 확인 (온보딩이 완료된 경우에만) + const newUser = parsed.queryParams?.newUser; + const isNewUser = newUser === 'true'; + console.log('✅ [카카오 로그인] 토큰 저장 완료'); - if (shouldOnboard) { + if (shouldOnboard || isNewUser) { navigation.replace('KakaoOnboarding'); } else { navigation.replace('Main'); @@ -190,9 +194,15 @@ const LoginScreen = ({navigation}: any) => { await AsyncStorage.setItem('membershipType', 'FREE'); } - // 온보딩 여부 확인 (isOnboarded 또는 onboarded 값 확인) + // 온보딩 여부 확인 (isOnboarded 우선 확인) + // isOnboarded === false: 온보딩 미완료 → 카카오 온보딩 const isOnboarded = data.isOnboarded !== undefined ? data.isOnboarded : data.onboarded; - if (isOnboarded === false) { + const shouldOnboard = isOnboarded === false; + + // 신규 유저 확인 (온보딩이 완료된 경우에만) + const isNewUser = data.newUser === true; + + if (shouldOnboard || isNewUser) { navigation.replace('KakaoOnboarding'); } else { navigation.replace('Main'); @@ -248,8 +258,18 @@ const LoginScreen = ({navigation}: any) => { await AsyncStorage.setItem('membershipType', 'FREE'); } - // 메인 화면으로 이동 - navigation.replace('Main'); + // 온보딩 여부 확인 (isOnboarded 우선 확인) + const isOnboarded = data.isOnboarded !== undefined ? data.isOnboarded : data.onboarded; + const shouldOnboard = isOnboarded === false; + + // 신규 유저 확인 (온보딩이 완료된 경우에만) + const isNewUser = data.newUser === true; + + if (shouldOnboard || isNewUser) { + navigation.replace('KakaoOnboarding'); + } else { + navigation.replace('Main'); + } } catch (error: any) { console.error('카카오 로그인 처리 실패:', error); } finally { @@ -281,7 +301,7 @@ const LoginScreen = ({navigation}: any) => { } // ✅ 방법 1: @react-native-seoul/kakao-login 사용 (네이티브 빌드에서만 작동) - if (KakaoLogin && KakaoLogin.login) { + if (KakaoLogin && typeof KakaoLogin.login === 'function') { try { console.log('🔵 [카카오 로그인] 네이티브 모듈 사용'); const token = await KakaoLogin.login(); @@ -293,6 +313,7 @@ const LoginScreen = ({navigation}: any) => { console.log('✅ [카카오 로그인] 프로필:', profile); // 백엔드 API 호출 (카카오 ID로 로그인/회원가입) + console.log('🔵 [카카오 로그인] 백엔드 API 호출 시작'); const res = await fetch( 'https://www.intelfits.com/api/auth/kakao/login', { @@ -307,11 +328,17 @@ const LoginScreen = ({navigation}: any) => { } ); + console.log('🔵 [카카오 로그인] 백엔드 응답 상태:', res.status); const data = await res.json(); - console.log('🔵 [카카오 로그인] 백엔드 응답:', data); + console.log('🔵 [카카오 로그인] 백엔드 응답 데이터:', data); if (!res.ok) { - throw new Error(data.message || '카카오 로그인에 실패했습니다'); + console.error('❌ [카카오 로그인] 백엔드 응답 실패:', { + status: res.status, + statusText: res.statusText, + data: data + }); + throw new Error(data.message || `카카오 로그인에 실패했습니다 (${res.status})`); } // 토큰 저장 @@ -330,17 +357,24 @@ const LoginScreen = ({navigation}: any) => { await AsyncStorage.setItem('membershipType', 'FREE'); } - // 온보딩 여부 확인 (isOnboarded 또는 onboarded 값 확인) + // 온보딩 여부 확인 (isOnboarded 우선 확인) const isOnboarded = data.isOnboarded !== undefined ? data.isOnboarded : data.onboarded; - if (isOnboarded === false) { + const shouldOnboard = isOnboarded === false; + + // 신규 유저 확인 (온보딩이 완료된 경우에만) + const isNewUser = data.newUser === true; + + if (shouldOnboard || isNewUser) { navigation.replace('KakaoOnboarding'); } else { navigation.replace('Main'); } return; } catch (nativeError: any) { + console.error('❌ [카카오 로그인] 네이티브 모듈 실패:', nativeError); console.log('⚠️ [카카오 로그인] 네이티브 모듈 실패, WebBrowser 방식으로 전환:', nativeError.message); // 네이티브 모듈 실패 시 WebBrowser 방식으로 폴백 + setLoading(false); // 로딩 상태 해제 } } @@ -351,10 +385,35 @@ const LoginScreen = ({navigation}: any) => { console.log('🔵 [카카오 로그인] WebBrowser 방식 사용'); console.log('🔵 [카카오 로그인] URL:', loginUrl); - const result = await WebBrowser.openAuthSessionAsync( - loginUrl, - 'intelfit://auth/kakao' // 딥링크 스킴 - ); + + // 딥링크 스킴 설정 (앱 내부 브라우저에서 열리도록) + const deepLinkScheme = 'intelfit://auth/kakao'; + console.log('🔵 [카카오 로그인] 딥링크 스킴:', deepLinkScheme); + + let result; + try { + // openAuthSessionAsync는 앱 내부 브라우저를 엽니다 + result = await WebBrowser.openAuthSessionAsync( + loginUrl, + deepLinkScheme + ); + } catch (browserError: any) { + console.error('❌ [카카오 로그인] WebBrowser 에러:', browserError); + // WebBrowser 실패 시 openBrowserAsync로 폴백 (앱 내부 브라우저) + console.log('🔄 [카카오 로그인] openBrowserAsync로 폴백'); + try { + await WebBrowser.openBrowserAsync(loginUrl); + // openBrowserAsync는 딥링크를 자동으로 처리하지 않으므로 + // Linking 이벤트 리스너가 처리하도록 함 + setLoading(false); + return; + } catch (fallbackError: any) { + console.error('❌ [카카오 로그인] openBrowserAsync도 실패:', fallbackError); + Alert.alert('오류', '카카오 로그인 페이지를 열 수 없습니다.'); + setLoading(false); + return; + } + } console.log('🔵 [카카오 로그인] 결과:', result); @@ -391,9 +450,21 @@ const LoginScreen = ({navigation}: any) => { const membershipType = (parsed.queryParams?.membershipType as string | undefined) || 'FREE'; await AsyncStorage.setItem('membershipType', membershipType); - console.log('✅ [카카오 로그인] 토큰 저장 완료, 메인 화면으로 이동'); - // 메인 화면으로 이동 - navigation.replace('Main'); + // 온보딩 여부 확인 (isOnboarded 우선 확인) + const isOnboarded = parsed.queryParams?.isOnboarded; + const onboarded = parsed.queryParams?.onboarded; + const shouldOnboard = isOnboarded === 'false' || onboarded === 'false'; + + // 신규 유저 확인 (온보딩이 완료된 경우에만) + const newUser = parsed.queryParams?.newUser; + const isNewUser = newUser === 'true'; + + console.log('✅ [카카오 로그인] 토큰 저장 완료'); + if (shouldOnboard || isNewUser) { + navigation.replace('KakaoOnboarding'); + } else { + navigation.replace('Main'); + } } catch (error: any) { console.error('❌ [카카오 로그인] 토큰 저장 실패:', error); Alert.alert('로그인 실패', error.message || '토큰 저장 중 오류가 발생했습니다.'); @@ -443,9 +514,10 @@ const LoginScreen = ({navigation}: any) => { await AsyncStorage.setItem('membershipType', 'FREE'); } - // 온보딩 여부 확인 - if (data.onboarded === false) { - navigation.replace('Onboarding'); + // 신규 유저/기존 유저 확인 (newUser 값 우선 확인) + const isNewUser = data.newUser === true; + if (isNewUser) { + navigation.replace('KakaoOnboarding'); } else { navigation.replace('Main'); } @@ -463,12 +535,15 @@ const LoginScreen = ({navigation}: any) => { } } else if (result.type === 'cancel') { console.log('⚠️ [카카오 로그인] 사용자가 취소함'); + Alert.alert('알림', '카카오 로그인이 취소되었습니다.'); } else { console.log('⚠️ [카카오 로그인] 예상치 못한 결과:', result); + Alert.alert('오류', '카카오 로그인에 실패했습니다. 다시 시도해주세요.'); } - } catch (error) { + } catch (error: any) { console.error('❌ [카카오 로그인] 에러:', error); - Alert.alert('오류', '카카오 로그인 페이지를 열 수 없습니다.'); + const errorMessage = error.message || '카카오 로그인 페이지를 열 수 없습니다.'; + Alert.alert('오류', errorMessage); } finally { setLoading(false); } diff --git a/src/screens/diet/MealAddScreen.tsx b/src/screens/diet/MealAddScreen.tsx index c9c0397..dca05bb 100644 --- a/src/screens/diet/MealAddScreen.tsx +++ b/src/screens/diet/MealAddScreen.tsx @@ -17,6 +17,7 @@ import {SafeAreaView} from 'react-native-safe-area-context'; import { Ionicons as Icon } from '@expo/vector-icons'; import * as ImagePicker from 'expo-image-picker'; import {useFocusEffect} from '@react-navigation/native'; +import AsyncStorage from '@react-native-async-storage/async-storage'; import FoodAddOptionsModal from '../../components/modals/FoodAddOptionsModal'; import FoodEditModal from '../../components/modals/FoodEditModal'; import FoodDirectInputModal from '../../components/modals/FoodDirectInputModal'; @@ -35,6 +36,7 @@ interface Food { protein: number; fat: number; weight: number; + liked?: boolean; // 좋아요 상태 } const MealAddScreen = ({navigation, route}: any) => { @@ -114,6 +116,7 @@ const MealAddScreen = ({navigation, route}: any) => { const [nutritionGoal, setNutritionGoal] = useState(null); const [isUploading, setIsUploading] = useState(false); const [dailyMealsData, setDailyMealsData] = useState(null); + const [likedFoods, setLikedFoods] = useState>(new Set()); // 좋아요한 음식 ID 집합 // 날짜 형식 변환 함수 (Date -> yyyy-MM-dd) const formatDateToString = (date: Date): string => { @@ -816,6 +819,39 @@ const MealAddScreen = ({navigation, route}: any) => { await mealAPI.addMeal(cleanMealRequestData as AddMealRequest); console.log('✅ 식단 수정 완료 (삭제 후 추가)'); + // 수정 모드: 하트 표시된(좋아요한) 음식들만 피드백 API 호출 + const userId = await AsyncStorage.getItem('userId'); + if (userId && likedFoods.size > 0) { + try { + // likedFoods에 있는 음식 ID들만 API 호출 (하트 표시된 것만) + const feedbackPromises = Array.from(likedFoods) + .filter(foodId => { + const food = foods.find(f => f.id === foodId); + return food && food.id > 0; // 유효한 음식만 필터링 + }) + .map(async (foodId) => { + const food = foods.find(f => f.id === foodId); + if (!food) return; + + // 하트 표시된 음식만 API 호출 + return mealAPI.submitFoodFeedback({ + user_id: userId, + food_id: food.id, + food_name: food.name, + feedback: "like", + }); + }); + + await Promise.all(feedbackPromises); + console.log('✅ 수정 모드 - 좋아요 피드백 전송 완료 (하트 표시된 음식만):', likedFoods.size, '개'); + } catch (feedbackError: any) { + console.error('⚠️ 수정 모드 - 좋아요 피드백 전송 실패:', feedbackError); + // 피드백 실패해도 식단 저장은 계속 진행 + } + } else { + console.log('ℹ️ 수정 모드 - 하트 표시된 음식이 없어 피드백 API를 호출하지 않습니다.'); + } + // 저장 후 GET으로 최신 데이터 불러오기 try { console.log('📥 저장 후 최신 식단 데이터 불러오기:', mealDate); @@ -858,6 +894,39 @@ const MealAddScreen = ({navigation, route}: any) => { // 추가 모드 await mealAPI.addMeal(cleanMealRequestData as AddMealRequest); + // 추가 모드: 하트 표시된(좋아요한) 음식들만 피드백 API 호출 + const userId = await AsyncStorage.getItem('userId'); + if (userId && likedFoods.size > 0) { + try { + // likedFoods에 있는 음식 ID들만 API 호출 (하트 표시된 것만) + const feedbackPromises = Array.from(likedFoods) + .filter(foodId => { + const food = foods.find(f => f.id === foodId); + return food && food.id > 0; // 유효한 음식만 필터링 + }) + .map(async (foodId) => { + const food = foods.find(f => f.id === foodId); + if (!food) return; + + // 하트 표시된 음식만 API 호출 + return mealAPI.submitFoodFeedback({ + user_id: userId, + food_id: food.id, + food_name: food.name, + feedback: "like", + }); + }); + + await Promise.all(feedbackPromises); + console.log('✅ 추가 모드 - 좋아요 피드백 전송 완료 (하트 표시된 음식만):', likedFoods.size, '개'); + } catch (feedbackError: any) { + console.error('⚠️ 추가 모드 - 좋아요 피드백 전송 실패:', feedbackError); + // 피드백 실패해도 식단 저장은 계속 진행 + } + } else { + console.log('ℹ️ 추가 모드 - 하트 표시된 음식이 없어 피드백 API를 호출하지 않습니다.'); + } + // 저장 후 GET으로 최신 데이터 불러오기 try { console.log('📥 저장 후 최신 식단 데이터 불러오기:', mealDate); @@ -1145,6 +1214,12 @@ const MealAddScreen = ({navigation, route}: any) => { // 음식 삭제 핸들러 const handleFoodDelete = (foodId: number) => { setFoods(prev => prev.filter(food => food.id !== foodId)); + // 좋아요 상태도 함께 제거 + setLikedFoods(prev => { + const newSet = new Set(prev); + newSet.delete(foodId); + return newSet; + }); }; // 정수 포맷팅 (소수점 제거) @@ -1256,7 +1331,31 @@ const MealAddScreen = ({navigation, route}: any) => { activeOpacity={0.7}> - {food.name} + + {food.name} + { + e.stopPropagation(); + const isLiked = likedFoods.has(food.id); + if (isLiked) { + setLikedFoods(prev => { + const newSet = new Set(prev); + newSet.delete(food.id); + return newSet; + }); + } else { + setLikedFoods(prev => new Set(prev).add(food.id)); + } + }} + activeOpacity={0.7}> + + + {Math.round(food.calories)}kcal { Alert.alert("성공", "오늘의 맞춤 식단이 생성되었습니다! 🎉"); } catch (error: any) { console.error("❌ 1일 식단 추천 실패:", error); - Alert.alert("오류", error.message || "식단을 불러오는데 실패했습니다."); + + // 500 에러인 경우 더 친절한 메시지 + let errorMessage = error.message || "식단을 불러오는데 실패했습니다."; + if (error.status === 500) { + errorMessage = "서버에 일시적인 문제가 발생했습니다.\n잠시 후 다시 시도해주세요."; + } + + Alert.alert("오류", errorMessage); } finally { setLoading(false); } diff --git a/src/screens/diet/TempMealRecommendScreen.tsx b/src/screens/diet/TempMealRecommendScreen.tsx index c5f15a3..2c350d6 100644 --- a/src/screens/diet/TempMealRecommendScreen.tsx +++ b/src/screens/diet/TempMealRecommendScreen.tsx @@ -326,7 +326,12 @@ const TempMealRecommendScreen: React.FC = () => { ) { setIsTokenDepleted(true); } else { - Alert.alert("오류", errorMessage || "식단 생성에 실패했습니다."); + // 500 에러인 경우 더 친절한 메시지 + let finalErrorMessage = errorMessage || "식단 생성에 실패했습니다."; + if (error.status === 500) { + finalErrorMessage = "서버에 일시적인 문제가 발생했습니다.\n잠시 후 다시 시도해주세요."; + } + Alert.alert("오류", finalErrorMessage); } } finally { setLoading(false); diff --git a/src/screens/main/MyPageScreen.tsx b/src/screens/main/MyPageScreen.tsx index fa01c98..630eb67 100644 --- a/src/screens/main/MyPageScreen.tsx +++ b/src/screens/main/MyPageScreen.tsx @@ -183,13 +183,47 @@ const MyPageScreen = ({ navigation }: any) => { ]); }; - // 단순 호출용 핸들러 (사용 안 함, handleLogoutPress 사용 권장) - const handleLogout = () => { - executeLogout(); - }; + const handleDeleteAccount = async () => { + // 프로필 데이터에서 카카오 사용자 여부 확인 + const isKakaoUser = profileData && ( + (profileData as any).loginType === "KAKAO" || + (profileData as any).provider === "KAKAO" || + (profileData as any).kakaoId !== undefined + ); - const handleDeleteAccount = () => { - setIsDeleteAccountModalOpen(true); + if (isKakaoUser) { + // 카카오 사용자: unlink API 호출 + try { + await authAPI.kakaoUnlink(); + + // 성공하면 카카오 사용자 → 탈퇴 완료 + Alert.alert( + "탈퇴 완료", + "카카오 연결이 해제되었습니다. 그동안 이용해주셔서 감사합니다.", + [ + { + text: "확인", + onPress: async () => { + // 로컬 데이터 삭제 + await AsyncStorage.removeItem("access_token"); + await AsyncStorage.removeItem("refresh_token"); + await AsyncStorage.removeItem("userId"); + await AsyncStorage.removeItem("userName"); + await AsyncStorage.removeItem("membershipType"); + await AsyncStorage.removeItem("chatbot_tokens"); + navigation.replace("Login"); + }, + }, + ] + ); + } catch (error: any) { + console.error("카카오 unlink 실패:", error); + Alert.alert("오류", error.message || "카카오 연결 해제에 실패했습니다."); + } + } else { + // 일반 사용자: 회원탈퇴 모달 열기 + setIsDeleteAccountModalOpen(true); + } }; const getMembershipTypeText = (type: string) => { diff --git a/src/services/authAPI.ts b/src/services/authAPI.ts index 08a8994..b6fc54b 100644 --- a/src/services/authAPI.ts +++ b/src/services/authAPI.ts @@ -845,15 +845,15 @@ export const authAPI = { * @returns 성공 여부 */ submitOnboarding: async (onboardingData: { + birthDate: string; + agreePrivacy: boolean; + agreeTerms: boolean; gender: "M" | "F"; height: number; weight: number; - birthDate: string; weightGoal: number; healthGoal: string; workoutDaysPerWeek: string; - experienceLevel?: string; - fitnessConcerns?: string; }): Promise<{ success: boolean; message: string }> => { try { const response = await request<{ success: boolean; message: string }>( diff --git a/src/services/mealAPI.ts b/src/services/mealAPI.ts index 05b3ec9..e5d2ebc 100644 --- a/src/services/mealAPI.ts +++ b/src/services/mealAPI.ts @@ -1,4 +1,4 @@ -import { request, AI_API_BASE_URL, ACCESS_TOKEN_KEY } from './apiConfig'; +import { request, requestAI, AI_API_BASE_URL, ACCESS_TOKEN_KEY } from './apiConfig'; import AsyncStorage from '@react-native-async-storage/async-storage'; import * as ImageManipulator from 'expo-image-manipulator'; import type { @@ -1484,4 +1484,26 @@ export const mealAPI = { throw new Error('사진 업로드에 실패했습니다. 다시 시도해주세요.'); } }, + + /** + * 음식 피드백 (좋아요) + * POST /food_feedback + */ + submitFoodFeedback: async (foodFeedback: { + user_id: string; + food_id: number; + food_name: string; + feedback: "like"; + }): Promise => { + try { + const response = await requestAI("/food_feedback", { + method: "POST", + body: JSON.stringify(foodFeedback), + }); + return response; + } catch (error: any) { + console.error("[MEAL] 음식 피드백 제출 실패:", error); + throw error; + } + }, }; diff --git a/src/services/recommendedMealAPI.ts b/src/services/recommendedMealAPI.ts index 86bd3b2..97f67b3 100644 --- a/src/services/recommendedMealAPI.ts +++ b/src/services/recommendedMealAPI.ts @@ -62,6 +62,12 @@ export const recommendedMealAPI = { } console.error("❌ 임시 식단 생성/조회 실패:", error); + + // 500 에러인 경우 더 명확한 메시지 제공 + if (error.status === 500) { + throw new Error("서버에 일시적인 문제가 발생했습니다. 잠시 후 다시 시도해주세요."); + } + throw new Error(error.message || "식단 생성에 실패했습니다."); } },