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 @@
+
+
+
+
+
+
+
+
+
+
+
+
@@ -232,6 +244,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
@@ -463,6 +487,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -947,6 +984,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1071,6 +1120,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 || "식단 생성에 실패했습니다.");
}
},