diff --git a/src/components/modals/DislikedFoodsModal.tsx b/src/components/modals/DislikedFoodsModal.tsx index 127f8df..bd5b277 100644 --- a/src/components/modals/DislikedFoodsModal.tsx +++ b/src/components/modals/DislikedFoodsModal.tsx @@ -11,61 +11,61 @@ import { Alert, ActivityIndicator, Dimensions, + Keyboard, } from "react-native"; import { Ionicons as Icon } from "@expo/vector-icons"; import { LinearGradient } from "expo-linear-gradient"; import { userPreferencesAPI } from "../../services/userPreferencesAPI"; +import type { ExclusionResponse } from "../../types"; const { width } = Dimensions.get("window"); interface DislikedFoodsModalProps { visible: boolean; onClose: () => void; - onSave?: (foods: string[]) => void; + userId: string; + onUpdate?: () => void; } const DislikedFoodsModal = ({ visible, onClose, - onSave, + userId, + onUpdate, }: DislikedFoodsModalProps) => { - const [dislikedFoods, setDislikedFoods] = useState([]); - const [originalFoods, setOriginalFoods] = useState([]); + const [dislikedFoods, setDislikedFoods] = useState([]); const [inputValue, setInputValue] = useState(""); - const [loading, setLoading] = useState(false); - const [hasChanges, setHasChanges] = useState(false); + const [initialLoading, setInitialLoading] = useState(false); + const [processing, setProcessing] = useState(false); - // 모달이 열릴 때 데이터 로드 useEffect(() => { - if (visible) { + if (visible && userId) { loadDislikedFoods(); } - }, [visible]); - - // 변경사항 감지 - useEffect(() => { - const changed = - JSON.stringify(dislikedFoods.sort()) !== - JSON.stringify(originalFoods.sort()); - setHasChanges(changed); - }, [dislikedFoods, originalFoods]); + }, [visible, userId]); + /** + * 비선호 식단 목록 불러오기 + */ const loadDislikedFoods = async () => { try { - setLoading(true); - const foods = await userPreferencesAPI.getDislikedFoods(); + setInitialLoading(true); + const foods = await userPreferencesAPI.getExclusions(userId); setDislikedFoods(foods); - setOriginalFoods(foods); - setHasChanges(false); } catch (error: any) { - console.error("Failed to load disliked foods", error); + console.error("비선호 식단 조회 실패:", error); Alert.alert("오류", "금지 식단을 불러오는데 실패했습니다."); } finally { - setLoading(false); + setInitialLoading(false); } }; - const handleAdd = () => { + /** + * 음식 추가 핸들러 + * - 쉼표로 구분된 여러 음식을 한 번에 입력 가능 + * - 예: "굴비, 다랑어" 또는 "오이" + */ + const handleAdd = async () => { const trimmed = inputValue.trim(); if (!trimmed) { @@ -73,73 +73,94 @@ const DislikedFoodsModal = ({ return; } - if (dislikedFoods.includes(trimmed)) { - Alert.alert("알림", "이미 추가된 음식입니다."); + // 쉼표로 구분하여 배열로 변환 (공백 제거) + const foodsToAdd = trimmed + .split(",") + .map((food) => food.trim()) + .filter((food) => food.length > 0); + + if (foodsToAdd.length === 0) { + Alert.alert("알림", "유효한 음식 이름을 입력해주세요."); return; } - setDislikedFoods((prev) => [...prev, trimmed]); - setInputValue(""); - }; + // 중복 검사 (기존 목록의 food_name과 비교) + const duplicates = foodsToAdd.filter((newFood) => + dislikedFoods.some((item) => { + // item.food_name이 "굴비, 다랑어" 형태일 수 있으므로 + // 쉼표로 분리해서 각각 비교 + const existingFoods = item.food_name.split(",").map((f) => f.trim()); + return existingFoods.includes(newFood); + }) + ); - const handleRemove = (food: string) => { - Alert.alert("삭제", `"${food}"를 삭제하시겠습니까?`, [ - { text: "취소", style: "cancel" }, - { - text: "삭제", - style: "destructive", - onPress: () => { - setDislikedFoods((prev) => prev.filter((f) => f !== food)); - }, - }, - ]); - }; + if (duplicates.length > 0) { + Alert.alert( + "알림", + `이미 목록에 있는 음식입니다: ${duplicates.join(", ")}` + ); + return; + } - const handleSave = async () => { try { - setLoading(true); + setProcessing(true); + Keyboard.dismiss(); - await userPreferencesAPI.saveDislikedFoods(dislikedFoods); + // API 호출: 배열 전달 + const newFood = await userPreferencesAPI.addExclusions( + userId, + foodsToAdd + ); - Alert.alert("성공", "금지 식단이 저장되었습니다."); - setOriginalFoods(dislikedFoods); - setHasChanges(false); + // 응답: { id: 1, food_name: "굴비, 다랑어", reason: "taste" } + // 목록에 추가 + setDislikedFoods((prev) => [...prev, newFood]); + setInputValue(""); - if (onSave) { - onSave(dislikedFoods); - } + Alert.alert("완료", `"${newFood.food_name}"이(가) 추가되었습니다.`); - onClose(); + if (onUpdate) onUpdate(); } catch (error: any) { - console.error("Failed to save disliked foods", error); - Alert.alert("오류", error.message || "저장에 실패했습니다."); + console.error("음식 추가 실패:", error); + Alert.alert("오류", "음식을 추가하지 못했습니다."); } finally { - setLoading(false); + setProcessing(false); } }; - const handleClose = () => { - if (hasChanges) { - Alert.alert( - "변경사항", - "저장하지 않은 변경사항이 있습니다. 나가시겠습니까?", - [ - { text: "취소", style: "cancel" }, - { - text: "나가기", - style: "destructive", - onPress: () => { - setDislikedFoods(originalFoods); - setInputValue(""); - setHasChanges(false); - onClose(); - }, - }, - ] - ); - } else { - onClose(); - } + /** + * 음식 삭제 핸들러 + * @param id exclusion_id (비선호 식단 저장한 식단의 id) + * @param name 표시용 음식 이름 (food_name) + */ + const handleRemove = (id: number, name: string) => { + Alert.alert("삭제", `"${name}"을(를) 제외 목록에서 삭제하시겠습니까?`, [ + { text: "취소", style: "cancel" }, + { + text: "삭제", + style: "destructive", + onPress: async () => { + try { + setProcessing(true); + + // API 호출: exclusion_id로 삭제 + await userPreferencesAPI.deleteExclusion(id); + + // 로컬 상태 업데이트 + setDislikedFoods((prev) => prev.filter((item) => item.id !== id)); + + Alert.alert("완료", "삭제되었습니다."); + + if (onUpdate) onUpdate(); + } catch (error: any) { + console.error("삭제 실패:", error); + Alert.alert("오류", "삭제에 실패했습니다."); + } finally { + setProcessing(false); + } + }, + }, + ]); }; return ( @@ -147,7 +168,7 @@ const DislikedFoodsModal = ({ visible={visible} transparent={true} animationType="slide" - onRequestClose={handleClose} + onRequestClose={onClose} > {/* 헤더 */} - + 금지 식단 설정 @@ -169,14 +190,13 @@ const DislikedFoodsModal = ({ - 알레르기나 선호하지 않는 음식을 추가하면 식단 추천에서 제외됩니다. + 쉼표(,)로 구분하여 여러 음식을 한 번에 추가할 수 있습니다. + {"\n"} + 예: 굴비, 다랑어, 오이 - + {/* 입력 필드 */} 음식 추가 @@ -193,103 +213,118 @@ const DislikedFoodsModal = ({ /> - + {processing ? ( + + ) : ( + + )} {/* 금지 식단 목록 */} - + 금지 식단 목록 ({dislikedFoods.length}) - {dislikedFoods.length === 0 ? ( - - - - 아직 추가된 금지 식단이 없습니다. - - - 위에서 음식을 추가해보세요. - + {initialLoading ? ( + + ) : ( - - {dislikedFoods.map((food, index) => ( - - - - - - - {food} + + {dislikedFoods.length === 0 ? ( + + + + 아직 추가된 금지 식단이 없습니다. + + + 위 입력창에서 원하는 음식을 추가해보세요. + + + ) : ( + + {dislikedFoods.map((item) => ( + + + + + + + + {item.food_name} + + + + + handleRemove(item.id, item.food_name) + } + style={styles.removeBtn} + disabled={processing} + > + + + - handleRemove(food)} - style={styles.removeBtn} - > - - - + ))} - ))} - + )} + + )} - + - {/* 저장 버튼 */} + {/* 닫기 버튼 */} - + - {loading ? ( - - ) : ( - <> - - - {hasChanges ? "저장하기" : "저장됨"} - - - )} + 닫기 @@ -329,7 +364,7 @@ const styles = StyleSheet.create({ }, infoBox: { flexDirection: "row", - alignItems: "center", + alignItems: "flex-start", backgroundColor: "#1e3a5f", padding: 16, marginHorizontal: 20, @@ -339,9 +374,9 @@ const styles = StyleSheet.create({ }, infoText: { flex: 1, - fontSize: 14, + fontSize: 13, color: "#ffffff", - lineHeight: 20, + lineHeight: 18, }, content: { flex: 1, @@ -349,7 +384,7 @@ const styles = StyleSheet.create({ paddingTop: 20, }, inputSection: { - marginBottom: 32, + marginBottom: 24, }, sectionTitle: { fontSize: 16, @@ -394,8 +429,11 @@ const styles = StyleSheet.create({ alignItems: "center", justifyContent: "center", }, - listSection: { - marginBottom: 100, + centerState: { + flex: 1, + justifyContent: "center", + alignItems: "center", + marginTop: 40, }, emptyState: { alignItems: "center", @@ -409,11 +447,14 @@ const styles = StyleSheet.create({ marginTop: 16, marginBottom: 8, }, - emptySubtext: { + emptySubText: { fontSize: 14, color: "#666666", textAlign: "center", }, + listContent: { + paddingBottom: 20, + }, foodList: { gap: 12, }, @@ -468,9 +509,6 @@ const styles = StyleSheet.create({ borderRadius: 12, overflow: "hidden", }, - saveBtnDisabled: { - opacity: 0.5, - }, saveBtnGradient: { flexDirection: "row", justifyContent: "center", diff --git a/src/screens/diet/MealRecommendScreen.tsx b/src/screens/diet/MealRecommendScreen.tsx index 0e83c86..2a6cc47 100644 --- a/src/screens/diet/MealRecommendScreen.tsx +++ b/src/screens/diet/MealRecommendScreen.tsx @@ -6,7 +6,6 @@ import { StyleSheet, ScrollView, TouchableOpacity, - TextInput, Alert, ActivityIndicator, Modal, @@ -18,9 +17,15 @@ import { SafeAreaView } from "react-native-safe-area-context"; import { Ionicons as Icon } from "@expo/vector-icons"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { useNavigation } from "@react-navigation/native"; -import { recommendedMealAPI, userPreferencesAPI } from "../../services"; +// 🔹 authAPI import 추가 +import { + recommendedMealAPI, + userPreferencesAPI, + authAPI, +} from "../../services"; import { LinearGradient } from "expo-linear-gradient"; import DislikedFoodsModal from "../../components/modals/DislikedFoodsModal"; +import type { ExclusionResponse } from "../../types"; const { width } = Dimensions.get("window"); @@ -32,6 +37,7 @@ const LOADING_MESSAGES = [ "거의 다 됐어요! 조금만 기다려주세요...", ]; +// ✅ 로딩 오버레이 컴포넌트 const LoadingOverlay = ({ visible, messages = LOADING_MESSAGES, @@ -268,7 +274,6 @@ const MealsSelectionModal = ({ colors={["rgba(26,26,46,0.98)", "rgba(22,33,62,0.98)"]} style={mealsModalStyles.content} > - {/* 헤더 */} 끼니 수 선택 @@ -278,7 +283,6 @@ const MealsSelectionModal = ({ 하루에 몇 끼를 드시나요? - {/* 끼니 선택 버튼들 */} {[1, 2, 3].map((num) => ( - {/* 닫기 버튼 */} { - console.log(`🔄 ${dayIndex}일차 변환 시작`); - let breakfast, lunch, dinner; if (tempDay.meals && tempDay.meals.length > 0) { @@ -470,8 +471,6 @@ const transformTempMealToUI = (tempDay: any, dayIndex: number) => { dinner = tempDay.meals.find((m: any) => m.mealType === "DINNER"); if (!breakfast && !lunch && !dinner) { - console.log(`⚠️ ${dayIndex}일차는 SNACK만 있음 - 변환 시작`); - const snacks = tempDay.meals.filter((m: any) => m.mealType === "SNACK"); if (snacks.length >= 1) { @@ -495,10 +494,6 @@ const transformTempMealToUI = (tempDay: any, dayIndex: number) => { mealTypeName: "저녁", }; } - - console.log( - `✅ SNACK 변환 완료: 아침=${!!breakfast}, 점심=${!!lunch}, 저녁=${!!dinner}` - ); } } @@ -592,18 +587,22 @@ const transformTempMealToUI = (tempDay: any, dayIndex: number) => { const MealRecommendScreen = () => { const navigation = useNavigation(); + const [userId, setUserId] = useState(""); + const [screen, setScreen] = useState<"welcome" | "meals">("welcome"); const [showDislikedModal, setShowDislikedModal] = useState(false); const [weeklyMeals, setWeeklyMeals] = useState([]); const [currentDay, setCurrentDay] = useState(0); - const [excludedIngredients, setExcludedIngredients] = useState([]); - const [newIngredient, setNewIngredient] = useState(""); + + const [excludedIngredients, setExcludedIngredients] = useState< + ExclusionResponse[] + >([]); + const [loading, setLoading] = useState(false); const [savedMeals, setSavedMeals] = useState([]); const [currentPlanId, setCurrentPlanId] = useState(null); - // ✅ 끼니 수 관련 state const [mealsPerDay, setMealsPerDay] = useState(3); const [showMealsModal, setShowMealsModal] = useState(false); @@ -617,38 +616,43 @@ const MealRecommendScreen = () => { }).start(); }, [screen]); + // 🔹 비선호 식단 목록 로드 함수 - userId를 파라미터로 받음 + const loadExclusions = async (currentUserId: string) => { + try { + const data = await userPreferencesAPI.getExclusions(currentUserId); + setExcludedIngredients(data); + console.log("✅ 비선호 음식 로드 완료:", data.length, "개"); + } catch (error) { + console.error("비선호 음식 로드 실패:", error); + } + }; + + // 🔹 유저 데이터 로드 함수 + const loadUserData = async () => { + try { + // 1. 프로필 조회하여 userId 획득 + const profile = await authAPI.getProfile(); + const currentUserId = profile.userId; + setUserId(currentUserId); + console.log("✅ 유저 ID 로드 완료:", currentUserId); + + // 2. 획득한 userId로 비선호 식단 조회 + await loadExclusions(currentUserId); + + // 3. 저장된 식단 불러오기 + await loadSavedMeals(); + } catch (error) { + console.error("사용자 데이터 로드 실패:", error); + } + }; + + // 🔹 useEffect에서 loadUserData 호출 useEffect(() => { - const loadData = async () => { - try { - console.log("========== GET 테스트 시작 =========="); - try { - const result = await userPreferencesAPI.getUserPreferences(); - console.log("✅ GET 성공:", result); - console.log("✅ 비선호 음식:", result.dislikedFoods); - } catch (testError) { - console.error("❌ GET 실패:", testError); - } - console.log("========== GET 테스트 완료 =========="); - - const dislikedFoods = await userPreferencesAPI.getDislikedFoods(); - setExcludedIngredients(dislikedFoods); - - console.log("✅ 비선호 음식 로드 완료:", dislikedFoods.length, "개"); - - await loadSavedMeals(); - } catch (error) { - console.error("데이터 로드 실패:", error); - try { - const stored = await AsyncStorage.getItem("excludedIngredients"); - if (stored) { - setExcludedIngredients(JSON.parse(stored)); - } - } catch (e) { - console.error("로컬 스토리지 읽기 실패:", e); - } - } + const init = async () => { + console.log("========== 초기 데이터 로드 =========="); + await loadUserData(); }; - loadData(); + init(); }, []); const loadSavedMeals = async () => { @@ -658,8 +662,6 @@ const MealRecommendScreen = () => { const serverPlans = await recommendedMealAPI.getSavedMealPlans(); - console.log("📦 서버에서 받은 plans:", serverPlans.length); - const bundleMap = new Map(); serverPlans.forEach((plan) => { @@ -689,16 +691,7 @@ const MealRecommendScreen = () => { description: `${bundle.mealCount}일 식단`, })); - console.log("✅ 그룹화된 서버 번들:", serverBundles.length); - const allMeals = [...localMeals, ...serverBundles]; - - console.log("📋 저장된 식단:", { - 로컬: localMeals.length, - 서버: serverBundles.length, - 합계: allMeals.length, - }); - setSavedMeals(allMeals); } catch (error) { console.error("저장된 식단 불러오기 실패:", error); @@ -725,29 +718,8 @@ const MealRecommendScreen = () => { try { console.log("🍽️ 임시 식단 생성 시작"); - console.log(`📊 끼니 수: ${mealsPerDay}끼`); - const tempMeals = await recommendedMealAPI.getWeeklyMealPlan(mealsPerDay); - console.log("=== 📦 API 응답 원본 ==="); - console.log("응답 배열 길이:", tempMeals.length); - console.log("전체 응답:", JSON.stringify(tempMeals, null, 2)); - - tempMeals.forEach((day, index) => { - console.log(`\n=== ${index + 1}일차 상세 ===`); - console.log("dayIndex:", day.dayIndex); - console.log("meals 배열:", day.meals); - console.log("meals 길이:", day.meals?.length || 0); - - day.meals?.forEach((meal, mealIdx) => { - console.log(` - ${meal.mealType}:`, { - id: meal.id, - foods개수: meal.foods?.length || 0, - totalCalories: meal.totalCalories, - }); - }); - }); - if (!tempMeals || tempMeals.length === 0) { throw new Error("식단 생성에 실패했습니다."); } @@ -756,27 +728,9 @@ const MealRecommendScreen = () => { const weekData = tempMeals.map((tempDay, index) => { const transformed = transformTempMealToUI(tempDay, index + 1); - - console.log(`\n=== ${index + 1}일차 변환 후 ===`); - console.log("totalCalories:", transformed.totalCalories); - console.log("아침 음식 수:", transformed.breakfast.meals.length); - console.log("점심 음식 수:", transformed.lunch.meals.length); - console.log("저녁 음식 수:", transformed.dinner.meals.length); - return transformed; }); - console.log("\n=== 📊 최종 weekData ==="); - console.log("weekData 길이:", weekData.length); - weekData.forEach((day, idx) => { - console.log(`${idx + 1}일차:`, { - totalCalories: day.totalCalories, - 아침: day.breakfast.meals.length, - 점심: day.lunch.meals.length, - 저녁: day.dinner.meals.length, - }); - }); - setWeeklyMeals(weekData); setCurrentPlanId(null); setScreen("meals"); @@ -800,8 +754,6 @@ const MealRecommendScreen = () => { try { console.log("🍽️ 1일 식단 생성 시작"); - console.log(`📊 끼니 수: ${mealsPerDay}끼`); - const tempMeals = await recommendedMealAPI.getDailyMealPlan(mealsPerDay); if (!tempMeals || tempMeals.length === 0) { @@ -822,14 +774,11 @@ const MealRecommendScreen = () => { Alert.alert("성공", "오늘의 맞춤 식단이 생성되었습니다! 🎉"); } catch (error: any) { console.error("❌ 1일 식단 추천 실패:", error); - - // 500 에러인 경우 더 친절한 메시지 let errorMessage = error.message || "식단을 불러오는데 실패했습니다."; if (error.status === 500) { errorMessage = "서버에 일시적인 문제가 발생했습니다.\n잠시 후 다시 시도해주세요."; } - Alert.alert("오류", errorMessage); } finally { setLoading(false); @@ -854,20 +803,6 @@ const MealRecommendScreen = () => { }; updated[currentDay] = dayMeals; - - console.log( - `${mealArray[mealIndex].liked ? "💚" : "🤍"} ${ - mealArray[mealIndex].name - } 좋아요 ${mealArray[mealIndex].liked ? "활성화" : "비활성화"}` - ); - - // TODO: 나중에 API 연결 - // if (mealArray[mealIndex].liked) { - // await userPreferencesAPI.addLikedFood(mealArray[mealIndex].name); - // } else { - // await userPreferencesAPI.removeLikedFood(mealArray[mealIndex].name); - // } - return updated; }); }; @@ -955,62 +890,6 @@ const MealRecommendScreen = () => { }); }; - const handleSaveMealPlanLocally = async () => { - try { - setLoading(true); - - const mealsForHistory = weeklyMeals.map((d) => ({ - totalCalories: d.totalCalories, - carbs: d.carbs, - protein: d.protein, - fat: d.fat, - breakfast: d.breakfast, - lunch: d.lunch, - dinner: d.dinner, - })); - - const mealPlanToSave = { - id: `local_${Date.now()}`, - date: new Date().toLocaleDateString("ko-KR"), - planName: weeklyMeals[0]?.planName || "AI 식단", - description: weeklyMeals[0]?.description || "AI가 생성한 식단", - totalCalories: weeklyMeals[0]?.totalCalories || 0, - totalCarbs: weeklyMeals[0]?.carbs || 0, - totalProtein: weeklyMeals[0]?.protein || 0, - totalFat: weeklyMeals[0]?.fat || 0, - createdAt: new Date().toISOString(), - meals: mealsForHistory, - isLocalMeal: true, - }; - - const stored = await AsyncStorage.getItem("savedMealPlans"); - const existingMeals = stored ? JSON.parse(stored) : []; - - const updatedMeals = [mealPlanToSave, ...existingMeals].slice(0, 20); - await AsyncStorage.setItem( - "savedMealPlans", - JSON.stringify(updatedMeals) - ); - - console.log("💾 로컬 저장 완료:", mealPlanToSave.id); - - Alert.alert("저장 완료", "식단이 기기에 저장되었습니다! 🎉", [ - { - text: "확인", - onPress: async () => { - await loadSavedMeals(); - setScreen("welcome"); - }, - }, - ]); - } catch (error: any) { - console.error("로컬 저장 실패:", error); - Alert.alert("오류", "식단 저장에 실패했습니다."); - } finally { - setLoading(false); - } - }; - const handleSaveMealPlan = async () => { try { setLoading(true); @@ -1062,10 +941,7 @@ const MealRecommendScreen = () => { "savedMealPlans", JSON.stringify(updatedMeals) ); - - console.log("🗑️ 로컬 식단 삭제:", meal.id); } else { - console.log("🗑️ 서버 번들 삭제:", meal.bundleId || meal.id); await recommendedMealAPI.deleteBundle(meal.bundleId || meal.id); } @@ -1100,7 +976,13 @@ const MealRecommendScreen = () => { onCancel={handleCancelLoading} /> - {/* ✅ 끼니 선택 모달 */} + setShowDislikedModal(false)} + onUpdate={() => loadExclusions(userId)} + /> + { - {/* 1. 7일 추천 식단 받기 */} { - {/* 2. 1일 추천 식단 받기 */} { - {/* 3. 금지 식재료 관리 */} setShowDislikedModal(true)} // 변경 + onPress={() => setShowDislikedModal(true)} activeOpacity={0.8} > { - {/* ✅ 4. 끼니 수정하기 */} setShowMealsModal(true)} @@ -1247,8 +1125,8 @@ const MealRecommendScreen = () => { 제외된 식재료 - {excludedIngredients.map((ingredient, index) => ( - + {excludedIngredients.map((item) => ( + { ]} style={styles.tagGradient} > - {ingredient} + {item.food_name} ))} @@ -1543,7 +1421,7 @@ const MealRecommendScreen = () => { - {/* ✅ 아침 */} + {/* 아침 */} { - {/* ✅ 점심 */} + {/* 점심 */} { - {/* ✅ 저녁 */} + {/* 저녁 */} ; const { width } = Dimensions.get("window"); @@ -37,6 +42,9 @@ const LOADING_MESSAGES = [ "거의 다 됐어요! 조금만 기다려주세요...", ]; +// ... (LoadingOverlay, MealsSelectionModal, mealsModalStyles, Interface 등은 기존과 동일하므로 생략) ... +// (위쪽 코드는 변경사항이 없으므로 그대로 두시면 됩니다. TempMealRecommendScreen 컴포넌트 내부만 수정합니다.) + const LoadingOverlay = ({ visible, messages = LOADING_MESSAGES, @@ -46,6 +54,7 @@ const LoadingOverlay = ({ messages?: string[]; onCancel?: () => void; }) => { + // ... 기존 코드 유지 const [currentMessageIndex, setCurrentMessageIndex] = useState(0); const fadeAnim = useRef(new Animated.Value(1)).current; const spinAnim = useRef(new Animated.Value(0)).current; @@ -176,7 +185,6 @@ const LoadingOverlay = ({ ); }; -// ✅ 끼니 선택 모달 const MealsSelectionModal = ({ visible, currentMeals, @@ -417,11 +425,15 @@ interface FoodItem { const TempMealRecommendScreen: React.FC = () => { const navigation = useNavigation(); + const [userId, setUserId] = useState(""); + const [screen, setScreen] = useState< "input" | "excludedIngredients" | "result" >("input"); const [currentDayTab, setCurrentDayTab] = useState(0); - const [excludedFoods, setExcludedFoods] = useState([]); + + const [excludedFoods, setExcludedFoods] = useState([]); + const [newIngredient, setNewIngredient] = useState(""); const [selectedPeriod, setSelectedPeriod] = useState<"daily" | "weekly">( "daily" @@ -429,11 +441,9 @@ const TempMealRecommendScreen: React.FC = () => { const [recommendedMeals, setRecommendedMeals] = useState([]); const [loading, setLoading] = useState(false); - // ✅ 끼니 수 관련 state const [mealsPerDay, setMealsPerDay] = useState(3); const [showMealsModal, setShowMealsModal] = useState(false); - // 토큰 부족 상태 관리 const [isTokenDepleted, setIsTokenDepleted] = useState(false); const [dailyNutrition, setDailyNutrition] = useState({ @@ -453,24 +463,29 @@ const TempMealRecommendScreen: React.FC = () => { }).start(); }, [screen]); + // 🔹 [변경] 데이터 로드 로직 수정: 프로필 조회 후 -> 식단 조회 useEffect(() => { - loadUserData(); + const init = async () => { + await loadUserData(); + }; + init(); }, []); const loadUserData = async () => { try { - const preferences = await userPreferencesAPI.getUserPreferences(); - if (preferences.dislikedFoods) { - setExcludedFoods(preferences.dislikedFoods); - } - console.log( - "✅ 비선호 음식 로드 완료:", - preferences.dislikedFoods?.length || 0, - "개" - ); + // 1. 프로필 조회하여 userId 획득 + const profile = await authAPI.getProfile(); + const currentUserId = profile.userId; + setUserId(currentUserId); + console.log("✅ 유저 ID 로드 완료:", currentUserId); + + // 2. 획득한 userId로 비선호 식단 조회 + const exclusions = await userPreferencesAPI.getExclusions(currentUserId); + setExcludedFoods(exclusions); + console.log("✅ 비선호 음식 로드 완료:", exclusions.length, "개"); } catch (error) { console.error("사용자 데이터 로드 실패:", error); - // 로컬 스토리지에서 불러오기 시도 + // 실패 시 로컬 스토리지 시도 (userId가 없으면 기능 제한될 수 있음) try { const stored = await AsyncStorage.getItem("excludedIngredients"); if (stored) { @@ -583,10 +598,10 @@ const TempMealRecommendScreen: React.FC = () => { ) { setIsTokenDepleted(true); } else { - // 500 에러인 경우 더 친절한 메시지 let finalErrorMessage = errorMessage || "식단 생성에 실패했습니다."; if (error.status === 500) { - finalErrorMessage = "서버에 일시적인 문제가 발생했습니다.\n잠시 후 다시 시도해주세요."; + finalErrorMessage = + "서버에 일시적인 문제가 발생했습니다.\n잠시 후 다시 시도해주세요."; } Alert.alert("오류", finalErrorMessage); } @@ -645,11 +660,20 @@ const TempMealRecommendScreen: React.FC = () => { const handleAddExcludedIngredient = async () => { const trimmed = newIngredient.trim(); - if (!trimmed) { + if (!trimmed) return; + if (!userId) { + Alert.alert( + "오류", + "유저 정보를 불러오는 중입니다. 잠시 후 다시 시도해주세요." + ); return; } - if (excludedFoods.includes(trimmed)) { + const isDuplicate = excludedFoods.some( + (item) => item.food_name === trimmed + ); + + if (isDuplicate) { Alert.alert("알림", "이미 추가된 식재료입니다."); return; } @@ -657,46 +681,19 @@ const TempMealRecommendScreen: React.FC = () => { try { setLoading(true); - try { - const result = await userPreferencesAPI.addDislikedFoods( - excludedFoods, - [trimmed] - ); - - setExcludedFoods(result.updatedList); - await AsyncStorage.setItem( - "excludedIngredients", - JSON.stringify(result.updatedList) - ); - - setNewIngredient(""); - console.log("✅ 비선호 음식 추가 완료 (서버):", result.updatedList); - return; - } catch (serverError: any) { - console.warn("⚠️ 서버 저장 실패, 로컬만 저장:", serverError.message); - - if ( - serverError.message?.includes("서버 내부 오류") || - serverError.status === 500 - ) { - const updatedList = [...excludedFoods, trimmed]; - setExcludedFoods(updatedList); - await AsyncStorage.setItem( - "excludedIngredients", - JSON.stringify(updatedList) - ); - - setNewIngredient(""); - Alert.alert( - "일부 성공", - "식재료가 기기에 저장되었습니다.\n(서버 동기화는 백엔드 수정 후 가능합니다)" - ); - console.log("✅ 비선호 음식 추가 완료 (로컬만):", updatedList); - return; - } + // 🔹 [변경] state로 저장된 userId 사용 + const newItem = await userPreferencesAPI.addExclusions(userId, [trimmed]); - throw serverError; - } + const updatedList = [...excludedFoods, newItem]; + setExcludedFoods(updatedList); + + await AsyncStorage.setItem( + "excludedIngredients", + JSON.stringify(updatedList) + ); + + setNewIngredient(""); + console.log("✅ 비선호 음식 추가 완료:", newItem); } catch (error: any) { console.error("비선호 음식 추가 실패:", error); Alert.alert("오류", error.message || "식재료 추가에 실패했습니다."); @@ -706,8 +703,8 @@ const TempMealRecommendScreen: React.FC = () => { }; // ✅ 금지 식재료 삭제 - const handleRemoveExcludedIngredient = async (ingredient: string) => { - Alert.alert("삭제", `"${ingredient}"를 삭제하시겠습니까?`, [ + const handleRemoveExcludedIngredient = async (id: number, name: string) => { + Alert.alert("삭제", `"${name}"를 삭제하시겠습니까?`, [ { text: "취소", style: "cancel" }, { text: "삭제", @@ -716,19 +713,17 @@ const TempMealRecommendScreen: React.FC = () => { try { setLoading(true); - const result = await userPreferencesAPI.removeDislikedFood( - excludedFoods, - ingredient - ); + await userPreferencesAPI.deleteExclusion(id); - setExcludedFoods(result.updatedList); + const updatedList = excludedFoods.filter((item) => item.id !== id); + setExcludedFoods(updatedList); await AsyncStorage.setItem( "excludedIngredients", - JSON.stringify(result.updatedList) + JSON.stringify(updatedList) ); - console.log("✅ 비선호 음식 삭제 완료:", result.updatedList); + console.log("✅ 비선호 음식 삭제 완료 (ID):", id); } catch (error: any) { console.error("비선호 음식 삭제 실패:", error); Alert.alert("오류", error.message || "식재료 삭제에 실패했습니다."); @@ -740,6 +735,9 @@ const TempMealRecommendScreen: React.FC = () => { ]); }; + // ... (나머지 render 코드는 UI만 그리므로 수정사항 없이 위에서 제공한 코드와 동일합니다. + // styles나 loadingStyles도 동일합니다.) + // ✅ input 화면 (웰컴 화면) if (screen === "input") { return ( @@ -756,7 +754,6 @@ const TempMealRecommendScreen: React.FC = () => { onCancel={handleCancelLoading} /> - {/* ✅ 끼니 선택 모달 */} { 제외된 식재료 - {excludedFoods.map((ingredient, index) => ( - + {excludedFoods.map((item) => ( + { ]} style={styles.tagGradient} > - {ingredient} + {item.food_name} ))} @@ -1122,9 +1119,9 @@ const TempMealRecommendScreen: React.FC = () => { - {excludedFoods.map((ingredient, index) => ( + {excludedFoods.map((item, index) => ( { - {ingredient} + + {item.food_name} + handleRemoveExcludedIngredient(ingredient)} + onPress={() => + handleRemoveExcludedIngredient(item.id, item.food_name) + } activeOpacity={0.7} > diff --git a/src/screens/exercise/ExerciseScreen.tsx b/src/screens/exercise/ExerciseScreen.tsx index ddda9a9..b736c17 100644 --- a/src/screens/exercise/ExerciseScreen.tsx +++ b/src/screens/exercise/ExerciseScreen.tsx @@ -218,14 +218,36 @@ const ExerciseScreen = ({ navigation }: any) => { const selectedDateStr = `${selectedDate.getFullYear()}-${String( selectedDate.getMonth() + 1 ).padStart(2, "0")}-${String(selectedDate.getDate()).padStart(2, "0")}`; + + // 🔍 디버깅 로그 + console.log("🔍 activities 필터링:", { + selectedDateStr, + allActivitiesCount: allActivities.length, + // AI 추천 운동이 있는지 확인 + aiActivities: allActivities.filter((a) => a.date === selectedDateStr), + }); + return allActivities.filter( (activity) => activity.date === selectedDateStr ); }, [allActivities, selectedDate]); - const workoutActivities = React.useMemo( - () => activities.filter((activity) => !isStretchActivity(activity)), - [activities] - ); + const workoutActivities = React.useMemo(() => { + const filtered = activities.filter( + (activity) => !isStretchActivity(activity) + ); + + // 🔍 디버깅 로그 + console.log("🏋️ workoutActivities 필터링:", { + activitiesCount: activities.length, + workoutActivitiesCount: filtered.length, + // AI 추천 운동이 포함되는지 확인 + aiWorkouts: filtered.filter((a) => a.saveTitle?.includes("AI")), + // 스트레칭으로 제외된 운동 + stretchActivities: activities.filter((a) => isStretchActivity(a)), + }); + + return filtered; + }, [activities]); const [savedWorkouts, setSavedWorkouts] = useState([]); const [savedWorkoutsLoading, setSavedWorkoutsLoading] = useState(false); @@ -582,11 +604,30 @@ const ExerciseScreen = ({ navigation }: any) => { // 완료되지 않은 운동이 있는지 확인 const hasIncompleteActivities = React.useMemo(() => { - return workoutActivities.some( + const result = workoutActivities.some( (activity) => !isActivityFullyCompleted(activity) ); - }, [workoutActivities, isActivityFullyCompleted]); + // 🔍 디버깅: 각 운동의 완료 상태 확인 + console.log("🎯 시작 버튼 조건:", { + hasIncomplete: result, + workoutActivitiesLength: workoutActivities.length, + // 각 운동별 상세 정보 + details: workoutActivities.map((a) => ({ + name: a.name, + isCompleted: a.isCompleted, + setsLength: a.sets?.length || 0, + setsDetail: a.sets?.map((s: any) => ({ + isCompleted: s.isCompleted, + weight: s.weight, + reps: s.reps, + })), + fullyCompleted: isActivityFullyCompleted(a), + })), + }); + + return result; + }, [workoutActivities, isActivityFullyCompleted]); const goalSummaryText = React.useMemo(() => { if (!goalData) { return "목표치가 아직 설정되지 않았습니다"; @@ -2517,32 +2558,32 @@ const ExerciseScreen = ({ navigation }: any) => { }; const handleStartWorkoutSequence = () => { - // 저장된 운동(saveTitle이 있는)은 제외하고 진행 가능한 운동만 필터링 + console.log("🚀 handleStartWorkoutSequence 실행 시작!"); + console.log("workoutActivities:", workoutActivities); + + // ✅ 수정: 저장된 세션(완료된 운동)만 제외 const availableActivities = workoutActivities.filter( - (activity) => !activity.saveTitle || activity.saveTitle.trim() === "" + (activity) => !isActivityFullyCompleted(activity) ); + console.log("🏃 시작 가능한 운동:", availableActivities.length); + if (availableActivities.length === 0) { + console.log("⚠️ 시작 가능한 운동이 없어서 운동 추가 모달 열기"); handleWorkoutStartPress(); return; } - // 운동 시작 시점의 누적 시간 기록 (현재 세션 시간 계산을 위해) + console.log("✅ 운동 시작:", availableActivities[0].name); + + // 운동 시작 시점의 누적 시간 기록 setWorkoutStartTime(todayTotalWorkoutSeconds); - // 현재 운동의 시작 시간도 기록 (각 운동별 시간 추적) setCurrentExerciseStartTime(todayTotalWorkoutSeconds); - // 실제 타임스탬프 기록 (실제 운동 시간 계산용) setCurrentExerciseStartTimestamp(Date.now()); - // 아직 완료하지 않은 첫 번째 운동을 찾음 - const nextActivity = - availableActivities.find( - (activity) => !isActivityFullyCompleted(activity) - ) || availableActivities[0]; - - const nextIndex = availableActivities.findIndex( - (activity) => activity.id === nextActivity.id - ); + // 첫 번째 미완료 운동 선택 + const nextActivity = availableActivities[0]; + const nextIndex = 0; // 이미지 찾기 const resolvedImageUrl = @@ -2554,20 +2595,17 @@ const ExerciseScreen = ({ navigation }: any) => { ? exerciseImagesByName[nextActivity.name.toLowerCase()] : null); - // ✅이미지 포함된 객체 생성 const activityWithImage = { ...nextActivity, imageUrl: resolvedImageUrl || undefined, }; - // 저장된 운동을 제외한 운동 목록만 시퀀스에 설정 setExerciseSequence(availableActivities); setExerciseSequenceIndex(nextIndex); setModalMode("edit"); setSelectedExercise(activityWithImage); setIsModalOpen(true); }; - const handleSequenceNavigate = (direction: "prev" | "next") => { if (!exerciseSequence.length) return; const delta = direction === "next" ? 1 : -1; @@ -2867,14 +2905,12 @@ const ExerciseScreen = ({ navigation }: any) => { // AI 추천 운동 수신 useFocusEffect( React.useCallback(() => { - // 1. 로딩 대기 if (!initialLoadComplete) return; - // 2. 파라미터 구조 유연하게 확인 (route.params 또는 route.params.params) const rawParams = (route.params as any) || {}; const recommendedExercises = rawParams.recommendedExercises || - rawParams.params?.recommendedExercises; // 👈 여기가 핵심! 한 단계 더 깊이 확인 + rawParams.params?.recommendedExercises; if ( recommendedExercises && @@ -2885,7 +2921,13 @@ const ExerciseScreen = ({ navigation }: any) => { `🚀 [AI] 추천 운동 ${recommendedExercises.length}개 수신 성공!` ); - // 3. 날짜 동기화 + // 🔍 디버깅: AI 추천 운동의 구조 확인 + console.log( + "🔍 AI 추천 운동 첫 번째:", + JSON.stringify(recommendedExercises[0], null, 2) + ); + + // 날짜 동기화 const routineDateStr = recommendedExercises[0].date; if (routineDateStr) { const [year, month, day] = routineDateStr.split("-").map(Number); @@ -2900,29 +2942,34 @@ const ExerciseScreen = ({ navigation }: any) => { } } - // 4. 데이터 추가 - setAllActivities((prev) => { - const existingGroupKey = recommendedExercises[0]?.groupKey; - const alreadyExists = prev.some( - (activity) => activity.groupKey === existingGroupKey - ); + // ⏱️ 약간의 딜레이 후 운동 추가 (State 업데이트 타이밍 보장) + setTimeout(() => { + setAllActivities((prev) => { + const existingGroupKey = recommendedExercises[0]?.groupKey; + const alreadyExists = prev.some( + (activity) => activity.groupKey === existingGroupKey + ); - if (alreadyExists) { - console.log("⚠️ [AI] 이미 추가된 루틴입니다."); - return prev; - } + if (alreadyExists) { + console.log("⚠️ [AI] 이미 추가된 루틴입니다."); + return prev; + } - console.log("✅ [AI] 리스트에 운동 추가 완료!"); - return [...prev, ...recommendedExercises]; - }); + console.log("✅ [AI] 리스트에 운동 추가 완료!"); + console.log( + "🔍 추가 후 전체 개수:", + prev.length + recommendedExercises.length + ); + + return [...prev, ...recommendedExercises]; + }); + }, 100); // 👈 100ms 딜레이 추가 - // 5. 파라미터 초기화 (재실행 방지) navigation.setParams({ recommendedExercises: undefined, - params: undefined, // 중첩된 params도 초기화 + params: undefined, }); - // 6. 알림 setTimeout(() => { Alert.alert( "루틴 추가 완료", @@ -3882,6 +3929,7 @@ const ExerciseScreen = ({ navigation }: any) => { + {/* 운동 기록 섹션 */} {/* 운동 기록 섹션 */} @@ -3889,14 +3937,48 @@ const ExerciseScreen = ({ navigation }: any) => { - {hasIncompleteActivities && workoutActivities.length > 0 ? ( - - 시작 - - ) : null} + {/* 🔍 디버깅: 조건 확인 */} + {(() => { + const shouldShow = + hasIncompleteActivities && workoutActivities.length > 0; + console.log("🚨 시작 버튼 렌더링 체크:", { + hasIncompleteActivities, + workoutActivitiesLength: workoutActivities.length, + shouldShow, + }); + + if (shouldShow) { + console.log("✅ 시작 버튼 렌더링됨!"); + } else { + console.log("❌ 시작 버튼 조건 불만족"); + } + + return shouldShow ? ( + { + console.log("🎯🎯🎯 시작 버튼 클릭됨!"); + console.log( + "workoutActivities:", + workoutActivities.length + ); + handleStartWorkoutSequence(); + }} + > + + 시작 + + + ) : null; + })()} @@ -3950,7 +4032,7 @@ const ExerciseScreen = ({ navigation }: any) => { diff --git a/src/services/apiConfig.ts b/src/services/apiConfig.ts index 6a25b97..4fac13d 100644 --- a/src/services/apiConfig.ts +++ b/src/services/apiConfig.ts @@ -29,6 +29,12 @@ export const request = async ( // 저장된 토큰 가져오기 const token = await AsyncStorage.getItem(ACCESS_TOKEN_KEY); + // 🔍 토큰 디버깅 로그 추가 + console.log( + "🔑 토큰 확인:", + token ? `토큰 있음 (${token.substring(0, 20)}...)` : "❌ 토큰 없음" + ); + // 요청 헤더 설정 const headers: HeadersInit = { "Content-Type": "application/json", @@ -42,27 +48,30 @@ export const request = async ( try { // 서버에 요청 전송 - console.log("API 요청:", `${API_BASE_URL}${endpoint}`); + // 📌 한글로 디코딩된 URL 로그 + const fullUrl = `${API_BASE_URL}${endpoint}`; + const decodedUrl = decodeURIComponent(fullUrl); + console.log("API 요청:", decodedUrl); console.log("요청 옵션:", { method: options.method || "GET", headers }); // POST/PUT 요청인 경우 본문도 로깅 - if ( - options.body && - (options.method === "POST" || - options.method === "PUT" || - options.method === "PATCH" || - options.method === "DELETE") - ) { - try { - const bodyData = - typeof options.body === "string" - ? JSON.parse(options.body) - : options.body; - console.log("요청 본문:", JSON.stringify(bodyData, null, 2)); - } catch (e) { - console.log("요청 본문 (원본):", options.body); - } -} + if ( + options.body && + (options.method === "POST" || + options.method === "PUT" || + options.method === "PATCH" || + options.method === "DELETE") + ) { + try { + const bodyData = + typeof options.body === "string" + ? JSON.parse(options.body) + : options.body; + console.log("요청 본문:", JSON.stringify(bodyData, null, 2)); + } catch (e) { + console.log("요청 본문 (원본):", options.body); + } + } const response = await fetch(`${API_BASE_URL}${endpoint}`, { ...options, diff --git a/src/services/userPreferencesAPI.ts b/src/services/userPreferencesAPI.ts index e3fa638..920339e 100644 --- a/src/services/userPreferencesAPI.ts +++ b/src/services/userPreferencesAPI.ts @@ -1,74 +1,119 @@ // src/services/userPreferencesAPI.ts -import { request } from "./apiConfig"; -import type { UserPreferencesResponse } from "../types"; +import { requestAI } from "./apiConfig"; +import type { ExclusionResponse, DeleteExclusionResponse } from "../types"; export const userPreferencesAPI = { /** - * 사용자 전체 선호도 조회 - * GET /api/user/preferences + * 📋 비선호(제외) 식단 목록 조회 + * GET /exclusions/{user_id} + * @param userId 사용자 ID (문자열, 예: "ehdrb") + * @returns 비선호 식단 목록 배열 + * + * 응답 예시: + * [ + * { id: 1, food_name: "굴비, 다랑어", reason: "taste" } + * ] */ - getUserPreferences: async (): Promise => { + getExclusions: async (userId: string): Promise => { try { - console.log("🍽️ 사용자 선호도 조회"); + console.log(`📋 비선호 식단 조회 요청 (User: ${userId})`); - const response = await request( - "/api/user/preferences", + const response = await requestAI( + `/exclusions/${userId}`, { method: "GET", } ); - console.log("✅ 선호도 조회 성공:", response); + console.log("✅ 비선호 식단 조회 성공:", response); return response; } catch (error: any) { - console.error("❌ 선호도 조회 실패:", error); - throw new Error(error.message || "사용자 선호도 조회에 실패했습니다."); + console.error("❌ 비선호 식단 조회 실패:", error); + throw new Error( + error.message || "비선호 식단 목록을 불러오는데 실패했습니다." + ); } }, /** - * 비선호 음식만 가져오기 (편의 함수) + * 🚫 비선호(제외) 식단 추가 + * POST /exclusions/{user_id}?food_name=굴비&food_name=다랑어&reason=taste + * @param userId 사용자 ID (문자열) + * @param foods 추가할 음식 이름 배열 (예: ["굴비", "다랑어"]) + * @returns 추가된 비선호 식단 정보 + * + * 응답 예시: + * { id: 1, food_name: "굴비, 다랑어", reason: "taste" } */ - getDislikedFoods: async (): Promise => { + addExclusions: async ( + userId: string, + foods: string[] + ): Promise => { try { - const preferences = await userPreferencesAPI.getUserPreferences(); - return preferences.dislikedFoods || []; + console.log(`🚫 비선호 식단 추가 요청 (User: ${userId})`); + console.log("추가할 음식:", foods); + + // 쿼리 파라미터 생성 + const queryParams = new URLSearchParams(); + foods.forEach((food) => { + queryParams.append("food_name", food); + }); + queryParams.append("reason", "taste"); + + const queryString = queryParams.toString(); + const url = `/exclusions/${userId}?${queryString}`; + + // 📌 한글로 디코딩된 URL 로그 출력 + const decodedUrl = `/exclusions/${userId}?${decodeURIComponent( + queryString + )}`; + console.log("🔗 요청 URL:", decodedUrl); + // 또는 더 명확하게 + console.log( + `POST /exclusions/${userId}?${foods + .map((f) => `food_name=${f}`) + .join("&")}&reason=taste` + ); + + const response = await requestAI(url, { + method: "POST", + }); + + console.log("✅ 비선호 식단 추가 성공:", response); + return response; } catch (error: any) { - console.error("❌ 비선호 음식 조회 실패:", error); - return []; + console.error("❌ 비선호 식단 추가 실패:", error); + throw new Error(error.message || "비선호 식단 추가에 실패했습니다."); } }, /** - * 금지 식단 전체 저장 (덮어쓰기) - * POST /api/user/preferences/disliked - * Body: { "dislikedFoods": ["땅콩", "우유", "새우"] } + * 🗑️ 비선호(제외) 식단 삭제 + * DELETE /exclusions/{exclusion_id} + * @param exclusionId 비선호 식단 저장한 식단의 id (조회 시 받은 id 값) + * @returns 삭제 결과 + * + * 응답 예시: + * { status: "deleted" } */ - saveDislikedFoods: async ( - dislikedFoods: string[] - ): Promise<{ dislikedFoods: string[] }> => { + deleteExclusion: async ( + exclusionId: number + ): Promise => { try { - console.log("💾 금지 식단 전체 저장:", dislikedFoods); - - const requestBody = { - dislikedFoods, - }; - - console.log("📤 요청 본문:", requestBody); + console.log(`🗑️ 비선호 식단 삭제 요청 (ID: ${exclusionId})`); - const response = await request<{ dislikedFoods: string[] }>( - "/api/user/preferences/disliked", + const response = await requestAI( + `/exclusions/${exclusionId}`, { - method: "POST", - body: JSON.stringify(requestBody), + method: "DELETE", } ); - console.log("✅ 금지 식단 저장 성공:", response); + console.log("✅ 비선호 식단 삭제 성공:", response); return response; } catch (error: any) { - console.error("❌ 금지 식단 저장 실패:", error); - throw new Error(error.message || "금지 식단 저장에 실패했습니다."); + console.error(`❌ 비선호 식단 삭제 실패 (ID: ${exclusionId}):`, error); + throw new Error(error.message || "비선호 식단 삭제에 실패했습니다."); } }, }; diff --git a/src/types/index.ts b/src/types/index.ts index 0ba1044..3e2fd07 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -210,18 +210,21 @@ export interface UserPreferencesResponse { } // 비선호 음식 추가 응답 타입 -export interface AddDislikedFoodResponse { - success: boolean; - message?: string; - dislikedFoods: string[]; +export interface ExclusionResponse { + id: number; + food_name: string; + reason: string; // "taste" } - // 비선호 음식 삭제 응답 타입 export interface RemoveDislikedFoodResponse { success: boolean; message?: string; dislikedFoods: string[]; } +//비선호 음식 삭제 응답 타입 +export interface DeleteExclusionResponse { + status: string; // 예: "deleted" +} // 홈 화면 응답 타입 export interface HomeResponse { userSummary: {