diff --git a/src/screens/auth/KakaoOnboardingScreen.tsx b/src/screens/auth/KakaoOnboardingScreen.tsx index b75f815..51dfcce 100644 --- a/src/screens/auth/KakaoOnboardingScreen.tsx +++ b/src/screens/auth/KakaoOnboardingScreen.tsx @@ -103,7 +103,7 @@ const KakaoOnboardingScreen = ({navigation}: any) => { const heightNum = Number(formData.height); if (heightNum < 100 || heightNum > 250) { newErrors.height = '키는 100cm 이상 250cm 이하여야 합니다'; - } + } } if (!formData.weight.trim()) { @@ -121,7 +121,7 @@ const KakaoOnboardingScreen = ({navigation}: any) => { const weightGoalNum = Number(formData.weightGoal); if (weightGoalNum < 30 || weightGoalNum > 200) { newErrors.weightGoal = '목표 체중은 30kg 이상 200kg 이하여야 합니다'; - } + } } if (!formData.healthGoal) { @@ -163,7 +163,7 @@ const KakaoOnboardingScreen = ({navigation}: any) => { }; const response = await authAPI.submitOnboarding(onboardingData); - + setLoading(false); // 200 응답 (온보딩 완료) → Alert 없이 바로 홈으로 이동 @@ -196,7 +196,7 @@ const KakaoOnboardingScreen = ({navigation}: any) => { }; return ( - @@ -410,41 +410,41 @@ const KakaoOnboardingScreen = ({navigation}: any) => { - { handleChange('gender', 'M'); setGenderModalVisible(false); }}> - - 남성 - - - + 남성 + + + { handleChange('gender', 'F'); setGenderModalVisible(false); }}> - - 여성 - - - + ]}> + 여성 + + + @@ -452,10 +452,10 @@ const KakaoOnboardingScreen = ({navigation}: any) => { {/* 키, 체중 */} - handleChange('height', text)} keyboardType="number-pad" placeholderTextColor="rgba(255, 255, 255, 0.7)" @@ -463,13 +463,13 @@ const KakaoOnboardingScreen = ({navigation}: any) => { {errors.height && ( {errors.height} )} - + - handleChange('weight', text)} keyboardType="number-pad" placeholderTextColor="rgba(255, 255, 255, 0.7)" @@ -501,8 +501,8 @@ const KakaoOnboardingScreen = ({navigation}: any) => { activeOpacity={0.8} style={styles.birthDateButtonContainer} onPress={() => setHealthGoalModalVisible(true)}> - opt.value === formData.healthGoal)?.label || '' @@ -543,26 +543,26 @@ const KakaoOnboardingScreen = ({navigation}: any) => { {healthGoalOptions.map((option) => ( - { handleChange('healthGoal', option.value); setHealthGoalModalVisible(false); }}> - + ]}> {option.label} - - - ))} - + + + ))} + @@ -616,26 +616,26 @@ const KakaoOnboardingScreen = ({navigation}: any) => { {workoutDaysOptions.map((option) => ( - { handleChange('workoutDaysPerWeek', option.value); setWorkoutDaysModalVisible(false); }}> - + ]}> {option.label} - - - ))} - + + + ))} + @@ -686,27 +686,27 @@ const KakaoOnboardingScreen = ({navigation}: any) => { {experienceLevelOptions.map((option) => ( - { handleChange('experienceLevel', option.value); setExperienceLevelModalVisible(false); }}> - + ]}> {option.label} - - - ))} - - + + + ))} + + @@ -722,20 +722,20 @@ const KakaoOnboardingScreen = ({navigation}: any) => { multiline /> - + - + onPress={handleSubmit} + disabled={loading}> {loading ? ( ) : ( 완료 )} - - - + + + ); }; diff --git a/src/screens/auth/LoginScreen.tsx b/src/screens/auth/LoginScreen.tsx index 97ee6ca..a60b493 100644 --- a/src/screens/auth/LoginScreen.tsx +++ b/src/screens/auth/LoginScreen.tsx @@ -139,7 +139,7 @@ const LoginScreen = ({navigation}: any) => { if (shouldOnboard || isNewUser) { navigation.replace('KakaoOnboarding'); } else { - navigation.replace('Main'); + navigation.replace('Main'); } } catch (error: any) { console.error('❌ [카카오 로그인] 토큰 저장 실패:', error); @@ -268,7 +268,7 @@ const LoginScreen = ({navigation}: any) => { if (shouldOnboard || isNewUser) { navigation.replace('KakaoOnboarding'); } else { - navigation.replace('Main'); + navigation.replace('Main'); } } catch (error: any) { console.error('카카오 로그인 처리 실패:', error); @@ -394,7 +394,7 @@ const LoginScreen = ({navigation}: any) => { try { // openAuthSessionAsync는 앱 내부 브라우저를 엽니다 result = await WebBrowser.openAuthSessionAsync( - loginUrl, + loginUrl, deepLinkScheme ); } catch (browserError: any) { @@ -463,7 +463,7 @@ const LoginScreen = ({navigation}: any) => { if (shouldOnboard || isNewUser) { navigation.replace('KakaoOnboarding'); } else { - navigation.replace('Main'); + navigation.replace('Main'); } } catch (error: any) { console.error('❌ [카카오 로그인] 토큰 저장 실패:', error); diff --git a/src/screens/diet/DietScreen.tsx b/src/screens/diet/DietScreen.tsx index b7ac0d0..f5e448d 100644 --- a/src/screens/diet/DietScreen.tsx +++ b/src/screens/diet/DietScreen.tsx @@ -47,8 +47,8 @@ const DietScreen = ({navigation, route}: any) => { // 진행률 API 사용 안 함 - 빈 배열로 유지 const [weeklyProgress, setWeeklyProgress] = useState([]); const [monthlyProgress, setMonthlyProgress] = useState([]); - // 칼로리 캐시 사용 안 함 - // const [dailyCaloriesCache, setDailyCaloriesCache] = useState>({}); + // 달력에 표시할 칼로리 데이터 (날짜별) + const [calendarCalories, setCalendarCalories] = useState>({}); // 추천 식단 관련 상태 const [savedMealPlans, setSavedMealPlans] = useState([]); @@ -117,6 +117,40 @@ const DietScreen = ({navigation, route}: any) => { return monthlyProgress.find((item) => item.date === dateStr); }; + // 달력에 표시할 날짜들의 칼로리 데이터 로드 + const loadCalendarCalories = async (dates: string[]) => { + try { + console.log('📅 [식단 화면] 달력 칼로리 데이터 로드 시작:', dates.length, '일'); + + // 각 날짜에 대해 영양성분 요약 조회 (병렬 처리) + const nutritionPromises = dates.map(async (date, index) => { + try { + console.log(`📡 [식단 화면] ${index + 1}/${dates.length} - ${date} 영양성분 조회 중...`); + const summary = await mealAPI.getNutritionSummary(date); + const calories = summary.calories || 0; + console.log(`✅ [식단 화면] ${index + 1}/${dates.length} - ${date} 칼로리: ${calories}kcal`); + return { date, calories }; + } catch (error) { + console.error(`❌ [식단 화면] ${index + 1}/${dates.length} - ${date} 영양성분 조회 실패:`, error); + return { date, calories: 0 }; + } + }); + + const nutritionResults = await Promise.all(nutritionPromises); + console.log('📅 [식단 화면] 달력 칼로리 데이터 조회 완료:', nutritionResults.length, '일'); + + // 상태 업데이트 + const caloriesMap: Record = {}; + nutritionResults.forEach(({ date, calories }) => { + caloriesMap[date] = calories; + }); + + setCalendarCalories(prev => ({ ...prev, ...caloriesMap })); + } catch (error) { + console.error('❌ [식단 화면] 달력 칼로리 데이터 로드 실패:', error); + } + }; + // 저장된 식단 플랜 목록 로드 const loadSavedMealPlans = async () => { try { @@ -475,15 +509,49 @@ const DietScreen = ({navigation, route}: any) => { // 다른 페이지에 갔다 오거나 운동 기록을 갔다 왔을 때, 탭 바꾸기 등 모든 행동 시 useFocusEffect( React.useCallback(() => { + // StatsScreen에서 날짜 처리를 하므로 여기서는 날짜를 변경하지 않음 + // 단지 현재 선택된 날짜를 사용하여 데이터 로드 const dateToFetch = selectedDate || new Date(); fetchDailyMeals(dateToFetch); loadSavedMealPlans().then(() => { loadRecommendedMealsForDate(dateToFetch); }); - // 진행률 API 호출 제거 + + // 달력 칼로리 데이터 새로고침 + if (showMonthView) { + // 월간 달력인 경우 해당 월의 모든 날짜 + const year = monthBase.getFullYear(); + const month = monthBase.getMonth() + 1; + const firstOfMonth = new Date(year, month - 1, 1); + const nextMonth = new Date(year, month, 1); + const daysInMonth = Math.round((nextMonth.getTime() - firstOfMonth.getTime()) / (1000 * 60 * 60 * 24)); + const monthDates = Array.from({ length: daysInMonth }).map((_, i) => { + const d = new Date(year, month - 1, i + 1); + return formatDateToString(d); + }); + loadCalendarCalories(monthDates); + } else { + // 주간 달력인 경우 이번 주 7일 + const getStartOfWeek = (d: Date) => { + const n = new Date(d.getFullYear(), d.getMonth(), d.getDate()); + const diff = n.getDay(); + n.setDate(n.getDate() - diff); + return n; + }; + const startOfWeek = getStartOfWeek(dateToFetch); + const weekDates = Array.from({ length: 7 }).map((_, i) => { + const d = new Date( + startOfWeek.getFullYear(), + startOfWeek.getMonth(), + startOfWeek.getDate() + i + ); + return formatDateToString(d); + }); + loadCalendarCalories(weekDates); + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedDate]) + }, [selectedDate, showMonthView, monthBase]) ); // 초기 데이터 로드 @@ -498,9 +566,46 @@ const DietScreen = ({navigation, route}: any) => { const year = monthBase.getFullYear(); const month = monthBase.getMonth() + 1; loadMonthlyProgress(year, month); + + // 해당 월의 모든 날짜에 대해 칼로리 데이터 로드 + const firstOfMonth = new Date(year, month - 1, 1); + const nextMonth = new Date(year, month, 1); + const daysInMonth = Math.round((nextMonth.getTime() - firstOfMonth.getTime()) / (1000 * 60 * 60 * 24)); + const monthDates = Array.from({ length: daysInMonth }).map((_, i) => { + const d = new Date(year, month - 1, i + 1); + return formatDateToString(d); + }); + loadCalendarCalories(monthDates); } }, [monthBase, showMonthView]); + // 주간 달력 칼로리 데이터 로드 + useEffect(() => { + if (!showMonthView) { + // 이번 주의 날짜 범위 계산 (일~토) + const today = new Date(); + const getStartOfWeek = (d: Date) => { + const n = new Date(d.getFullYear(), d.getMonth(), d.getDate()); + const diff = n.getDay(); + n.setDate(n.getDate() - diff); + return n; + }; + const dateToShow = selectedDate || today; + const startOfWeek = getStartOfWeek(dateToShow); + + const weekDates = Array.from({ length: 7 }).map((_, i) => { + const d = new Date( + startOfWeek.getFullYear(), + startOfWeek.getMonth(), + startOfWeek.getDate() + i + ); + return formatDateToString(d); + }); + + loadCalendarCalories(weekDates); + } + }, [selectedDate, showMonthView]); + // 영양 목표 로드 (특정 날짜의 목표 조회 API 사용) @@ -829,7 +934,9 @@ const DietScreen = ({navigation, route}: any) => { {(() => { const dayProgress = getDayProgress(d); - const calories = dayProgress?.totalCalorie || 0; + const dateStr = formatDateToString(d); + // 달력 칼로리 데이터 우선 사용, 없으면 진행률 데이터 사용 + const calories = calendarCalories[dateStr] ?? dayProgress?.totalCalorie ?? 0; const rate = dayProgress?.exerciseRate || 0; return ( <> @@ -908,7 +1015,9 @@ const DietScreen = ({navigation, route}: any) => { {(() => { const dayProgress = getDayProgress(d); - const calories = dayProgress?.totalCalorie || 0; + const dateStr = formatDateToString(d); + // 달력 칼로리 데이터 우선 사용, 없으면 진행률 데이터 사용 + const calories = calendarCalories[dateStr] ?? dayProgress?.totalCalorie ?? 0; const rate = dayProgress?.exerciseRate || 0; return ( <> @@ -1254,7 +1363,11 @@ const DietScreen = ({navigation, route}: any) => { {/* 영양 목표 설정 모달 */} setIsNutritionModalOpen(false)} + onClose={() => { + setIsNutritionModalOpen(false); + // 모달 닫을 때 영양 목표 다시 불러오기 + loadNutritionGoal(); + }} currentGoal={nutritionGoal} onGoalUpdate={() => { loadNutritionGoal(); diff --git a/src/screens/diet/MealAddScreen.tsx b/src/screens/diet/MealAddScreen.tsx index dca05bb..8c21026 100644 --- a/src/screens/diet/MealAddScreen.tsx +++ b/src/screens/diet/MealAddScreen.tsx @@ -1332,7 +1332,7 @@ const MealAddScreen = ({navigation, route}: any) => { - {food.name} + {food.name} { diff --git a/src/screens/exercise/ExerciseScreen.tsx b/src/screens/exercise/ExerciseScreen.tsx index 9c9431a..2633807 100644 --- a/src/screens/exercise/ExerciseScreen.tsx +++ b/src/screens/exercise/ExerciseScreen.tsx @@ -40,6 +40,7 @@ import { eventBus } from "../../utils/eventBus"; import { useDate } from "../../contexts/DateContext"; import type { DailyProgressWeekItem } from "../../types"; import { API_BASE_URL, ACCESS_TOKEN_KEY } from "../../services/apiConfig"; +import { mealAPI } from "../../services"; import { useFocusEffect, useRoute } from "@react-navigation/native"; interface Activity { id: number; @@ -476,6 +477,8 @@ const ExerciseScreen = ({ navigation }: any) => { const [monthlyProgress, setMonthlyProgress] = useState< DailyProgressWeekItem[] >([]); + // 달력에 표시할 칼로리 데이터 (날짜별) + const [calendarCalories, setCalendarCalories] = useState>({}); const [showMonthView, setShowMonthView] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); const [isIntroVisible, setIsIntroVisible] = useState(false); @@ -2081,6 +2084,40 @@ const ExerciseScreen = ({ navigation }: any) => { ); }; + // 달력에 표시할 날짜들의 칼로리 데이터 로드 + const loadCalendarCalories = async (dates: string[]) => { + try { + console.log('📅 [운동 화면] 달력 칼로리 데이터 로드 시작:', dates.length, '일'); + + // 각 날짜에 대해 영양성분 요약 조회 (병렬 처리) + const nutritionPromises = dates.map(async (date, index) => { + try { + console.log(`📡 [운동 화면] ${index + 1}/${dates.length} - ${date} 영양성분 조회 중...`); + const summary = await mealAPI.getNutritionSummary(date); + const calories = summary.calories || 0; + console.log(`✅ [운동 화면] ${index + 1}/${dates.length} - ${date} 칼로리: ${calories}kcal`); + return { date, calories }; + } catch (error) { + console.error(`❌ [운동 화면] ${index + 1}/${dates.length} - ${date} 영양성분 조회 실패:`, error); + return { date, calories: 0 }; + } + }); + + const nutritionResults = await Promise.all(nutritionPromises); + console.log('📅 [운동 화면] 달력 칼로리 데이터 조회 완료:', nutritionResults.length, '일'); + + // 상태 업데이트 + const caloriesMap: Record = {}; + nutritionResults.forEach(({ date, calories }) => { + caloriesMap[date] = calories; + }); + + setCalendarCalories(prev => ({ ...prev, ...caloriesMap })); + } catch (error) { + console.error('❌ [운동 화면] 달력 칼로리 데이터 로드 실패:', error); + } + }; + // 월별 데이터 로드 const loadMonthlyProgress = async (year: number, month: number) => { try { @@ -2097,6 +2134,18 @@ const ExerciseScreen = ({ navigation }: any) => { React.useEffect(() => { if (showMonthView) { loadMonthlyProgress(monthBase.getFullYear(), monthBase.getMonth()); + + // 해당 월의 모든 날짜에 대해 칼로리 데이터 로드 + const year = monthBase.getFullYear(); + const month = monthBase.getMonth(); + const firstOfMonth = new Date(year, month, 1); + const nextMonth = new Date(year, month + 1, 1); + const daysInMonth = Math.round((nextMonth.getTime() - firstOfMonth.getTime()) / (1000 * 60 * 60 * 24)); + const monthDates = Array.from({ length: daysInMonth }).map((_, i) => { + const d = new Date(year, month, i + 1); + return formatDateToString(d); + }); + loadCalendarCalories(monthDates); } }, [monthBase, showMonthView]); @@ -2106,9 +2155,39 @@ const ExerciseScreen = ({ navigation }: any) => { if (showMonthView) { // 달력을 펼칠 때 monthBase의 달 데이터 가져오기 loadMonthlyProgress(monthBase.getFullYear(), monthBase.getMonth()); + + // 해당 월의 모든 날짜에 대해 칼로리 데이터 로드 + const year = monthBase.getFullYear(); + const month = monthBase.getMonth(); + const firstOfMonth = new Date(year, month, 1); + const nextMonth = new Date(year, month + 1, 1); + const daysInMonth = Math.round((nextMonth.getTime() - firstOfMonth.getTime()) / (1000 * 60 * 60 * 24)); + const monthDates = Array.from({ length: daysInMonth }).map((_, i) => { + const d = new Date(year, month, i + 1); + return formatDateToString(d); + }); + loadCalendarCalories(monthDates); } else { // 달력을 접을 때 선택된 날짜의 달 데이터 가져오기 (주간 달력 표시 시) loadMonthlyProgress(dateToFetch.getFullYear(), dateToFetch.getMonth()); + + // 이번 주의 날짜 범위 계산 (일~토) + const getStartOfWeek = (d: Date) => { + const n = new Date(d.getFullYear(), d.getMonth(), d.getDate()); + const diff = n.getDay(); + n.setDate(n.getDate() - diff); + return n; + }; + const startOfWeek = getStartOfWeek(dateToFetch); + const weekDates = Array.from({ length: 7 }).map((_, i) => { + const d = new Date( + startOfWeek.getFullYear(), + startOfWeek.getMonth(), + startOfWeek.getDate() + i + ); + return formatDateToString(d); + }); + loadCalendarCalories(weekDates); } }, [showMonthView, selectedDate]); @@ -2116,7 +2195,27 @@ const ExerciseScreen = ({ navigation }: any) => { React.useEffect(() => { const dateToFetch = selectedDate || new Date(); loadMonthlyProgress(dateToFetch.getFullYear(), dateToFetch.getMonth()); - }, [selectedDate]); + + // 주간 달력인 경우 이번 주 7일의 칼로리 데이터 로드 + if (!showMonthView) { + const getStartOfWeek = (d: Date) => { + const n = new Date(d.getFullYear(), d.getMonth(), d.getDate()); + const diff = n.getDay(); + n.setDate(n.getDate() - diff); + return n; + }; + const startOfWeek = getStartOfWeek(dateToFetch); + const weekDates = Array.from({ length: 7 }).map((_, i) => { + const d = new Date( + startOfWeek.getFullYear(), + startOfWeek.getMonth(), + startOfWeek.getDate() + i + ); + return formatDateToString(d); + }); + loadCalendarCalories(weekDates); + } + }, [selectedDate, showMonthView]); // 화면 포커스 시 목표/진행 재로딩 // 다른 페이지에 갔다 오거나 식단 기록을 갔다 왔을 때, 탭 바꾸기 등 모든 행동 시 @@ -2124,15 +2223,51 @@ const ExerciseScreen = ({ navigation }: any) => { useFocusEffect( React.useCallback(() => { if (!userIdLoaded) return; - // 화면 포커스 시 날짜와 달력 월을 오늘로 설정 - const today = new Date(); - setSelectedDate(today); - setMonthBase(new Date(today.getFullYear(), today.getMonth(), 1)); + + // StatsScreen에서 날짜 처리를 하므로 여기서는 날짜를 변경하지 않음 + // 단지 현재 선택된 날짜를 사용하여 데이터 로드 + const dateToFetch = selectedDate || new Date(); + setMonthBase(new Date(dateToFetch.getFullYear(), dateToFetch.getMonth(), 1)); + loadGoalData(); loadWeeklyCalories(); // 해당 달의 월별 데이터 가져오기 - loadMonthlyProgress(today.getFullYear(), today.getMonth()); - }, [userIdLoaded, loadGoalData, loadWeeklyCalories, setSelectedDate]) + loadMonthlyProgress(dateToFetch.getFullYear(), dateToFetch.getMonth()); + + // 달력 칼로리 데이터 새로고침 + const dateToUse = selectedDate || today; + if (showMonthView) { + // 월간 달력인 경우 해당 월의 모든 날짜 + const year = dateToUse.getFullYear(); + const month = dateToUse.getMonth(); + const firstOfMonth = new Date(year, month, 1); + const nextMonth = new Date(year, month + 1, 1); + const daysInMonth = Math.round((nextMonth.getTime() - firstOfMonth.getTime()) / (1000 * 60 * 60 * 24)); + const monthDates = Array.from({ length: daysInMonth }).map((_, i) => { + const d = new Date(year, month, i + 1); + return formatDateToString(d); + }); + loadCalendarCalories(monthDates); + } else { + // 주간 달력인 경우 이번 주 7일 + const getStartOfWeek = (d: Date) => { + const n = new Date(d.getFullYear(), d.getMonth(), d.getDate()); + const diff = n.getDay(); + n.setDate(n.getDate() - diff); + return n; + }; + const startOfWeek = getStartOfWeek(dateToUse); + const weekDates = Array.from({ length: 7 }).map((_, i) => { + const d = new Date( + startOfWeek.getFullYear(), + startOfWeek.getMonth(), + startOfWeek.getDate() + i + ); + return formatDateToString(d); + }); + loadCalendarCalories(weekDates); + } + }, [userIdLoaded, loadGoalData, loadWeeklyCalories, showMonthView, selectedDate]) ); // 완료 횟수 저장 helper @@ -3446,7 +3581,9 @@ const ExerciseScreen = ({ navigation }: any) => { {(() => { const dayProgress = getDayProgress(d); - const calories = dayProgress?.totalCalorie || 0; + const dateStr = formatDateToString(d); + // 달력 칼로리 데이터 우선 사용, 없으면 진행률 데이터 사용 + const calories = calendarCalories[dateStr] ?? dayProgress?.totalCalorie ?? 0; const rate = dayProgress?.exerciseRate || 0; return ( <> @@ -3535,7 +3672,9 @@ const ExerciseScreen = ({ navigation }: any) => { {(() => { const dayProgress = getDayProgress(d); - const calories = dayProgress?.totalCalorie || 0; + const dateStr = formatDateToString(d); + // 달력 칼로리 데이터 우선 사용, 없으면 진행률 데이터 사용 + const calories = calendarCalories[dateStr] ?? dayProgress?.totalCalorie ?? 0; const rate = dayProgress?.exerciseRate || 0; return ( <> diff --git a/src/screens/main/HomeScreen.tsx b/src/screens/main/HomeScreen.tsx index 68fd0c5..a88731f 100644 --- a/src/screens/main/HomeScreen.tsx +++ b/src/screens/main/HomeScreen.tsx @@ -73,19 +73,90 @@ const HomeScreen = ({ navigation }: any) => { // 주간 진행률 데이터 로드 const loadWeeklyProgress = async () => { try { + console.log('🏠 [홈 화면] 주간 진행률 데이터 로드 시작'); const data = await homeAPI.getWeeklyProgress(); + console.log('🏠 [홈 화면] 주간 진행률 데이터 수신 완료:', data); + // 이번 주의 날짜 범위 계산 (일~토) + const today = new Date(); + const getStartOfWeek = (d: Date) => { + const n = new Date( + d.getFullYear(), + d.getMonth(), + d.getDate() + ); + const diff = n.getDay(); + n.setDate(n.getDate() - diff); + return n; + }; + const startOfWeek = getStartOfWeek(today); + + // 이번 주의 각 날짜에 대해 칼로리 데이터 가져오기 + const weekDates = Array.from({ length: 7 }).map((_, i) => { + const d = new Date( + startOfWeek.getFullYear(), + startOfWeek.getMonth(), + startOfWeek.getDate() + i + ); + return formatDateToString(d); + }); + + console.log('🏠 [홈 화면] 이번 주 날짜 (7일):', weekDates); + + // 각 날짜에 대해 영양성분 요약 조회 (병렬 처리 - 7번 호출) + console.log('🏠 [홈 화면] 이번 주 칼로리 데이터 조회 시작 (7일 병렬 호출)'); + const nutritionPromises = weekDates.map(async (date, index) => { + try { + console.log(`📡 [홈 화면] ${index + 1}/7 - ${date} 영양성분 조회 중...`); + const summary = await mealAPI.getNutritionSummary(date); + const calories = summary.calories || 0; + console.log(`✅ [홈 화면] ${index + 1}/7 - ${date} 칼로리: ${calories}kcal`); + return { date, calories }; + } catch (error) { + console.error(`❌ [홈 화면] ${index + 1}/7 - ${date} 영양성분 조회 실패:`, error); + return { date, calories: 0 }; + } + }); + + const nutritionResults = await Promise.all(nutritionPromises); + console.log('🏠 [홈 화면] 이번 주 칼로리 데이터 조회 완료 (7일):', nutritionResults); + + // 기존 데이터와 병합 (칼로리 데이터 업데이트) + let updatedData: DailyProgressWeekItem[] = []; + if (Array.isArray(data) && data.length > 0) { - setWeeklyProgress(data); + // 기존 데이터를 기반으로 업데이트 + updatedData = weekDates.map((date) => { + const existingItem = data.find((item) => item.date === date); + const nutritionItem = nutritionResults.find((item) => item.date === date); + + return { + date, + exerciseRate: existingItem?.exerciseRate ?? 0, + totalCalorie: nutritionItem?.calories ?? existingItem?.totalCalorie ?? 0, + }; + }); } else { - setWeeklyProgress([]); + // 기존 데이터가 없으면 영양성분 데이터만 사용 + updatedData = weekDates.map((date) => { + const nutritionItem = nutritionResults.find((item) => item.date === date); + return { + date, + exerciseRate: 0, + totalCalorie: nutritionItem?.calories ?? 0, + }; + }); } + + setWeeklyProgress(updatedData); + console.log('🏠 [홈 화면] 주간 진행률 상태 업데이트 완료:', updatedData.length, '개'); } catch (e: any) { - console.error("주간 진행률 로드 실패:", e); + console.error("❌ [홈 화면] 주간 진행률 로드 실패:", e); setWeeklyProgress([]); } }; + // 특정 날짜의 진행률 데이터 가져오기 const getDayProgress = (date: Date): DailyProgressWeekItem | undefined => { const dateStr = formatDateToString(date); @@ -519,22 +590,23 @@ const HomeScreen = ({ navigation }: any) => { // 화면 포커스 시 데이터 로드 useEffect(() => { const unsubscribe = navigation.addListener("focus", () => { - console.log("[HOME] 화면 포커스, 오늘 운동 데이터 새로고침 시작"); + console.log("[HOME] 화면 포커스, 데이터 새로고침 시작 (이번 주 칼로리 포함)"); if (isLoadingRef.current) { + console.log("[HOME] 이미 로딩 중이므로 스킵"); return; } isLoadingRef.current = true; Promise.all([ - loadWeeklyProgress(), + loadWeeklyProgress(), // 이번 주 칼로리 데이터 포함 loadHomeData(), loadTodayWorkoutTime(), loadInBodyData(), loadProfileInfo(), ]).finally(() => { isLoadingRef.current = false; - console.log("[HOME] 화면 포커스, 오늘 운동 데이터 새로고침 완료"); + console.log("[HOME] 화면 포커스, 데이터 새로고침 완료 (이번 주 칼로리 포함)"); }); // 코치 리포트가 있으면 새로운 랜덤 선택 (API 호출 없이) diff --git a/src/screens/main/StatsScreen.tsx b/src/screens/main/StatsScreen.tsx index bb898a7..01496e0 100644 --- a/src/screens/main/StatsScreen.tsx +++ b/src/screens/main/StatsScreen.tsx @@ -1,4 +1,4 @@ -import React, {useState, useEffect, useCallback} from 'react'; +import React, {useState, useEffect, useCallback, useRef} from 'react'; import { View, Text, @@ -9,14 +9,19 @@ import { import {SafeAreaView} from 'react-native-safe-area-context'; import {colors} from '../../theme/colors'; import AsyncStorage from '@react-native-async-storage/async-storage'; +import {useFocusEffect} from '@react-navigation/native'; import ExerciseScreen from '../exercise/ExerciseScreen'; import DietScreen from '../diet/DietScreen'; +import {useDate} from '../../contexts/DateContext'; const StatsScreen = ({navigation, route}: any) => { const [activeTab, setActiveTab] = useState(0); const [goalData, setGoalData] = useState(null); const [userId, setUserId] = useState(null); const [userIdLoaded, setUserIdLoaded] = useState(false); + const {setSelectedDate} = useDate(); + const previousActiveTabRef = useRef(null); + const lastFocusTimeRef = useRef(0); const storageKey = React.useMemo( () => (userId ? `workoutGoals:${userId}` : 'workoutGoals'), @@ -73,6 +78,36 @@ const StatsScreen = ({navigation, route}: any) => { } }, [route?.params?.activeTab, navigation]); + // 화면 포커스 시: 다른 탭에서 돌아온 경우 날짜를 오늘로 설정 + useFocusEffect( + useCallback(() => { + const now = Date.now(); + const timeSinceLastFocus = now - lastFocusTimeRef.current; + const previousActiveTab = previousActiveTabRef.current; + + // activeTab이 변경되었으면 Stats 내부에서 탭 전환한 것으로 간주 (날짜 유지) + const isTabSwitch = previousActiveTab !== null && previousActiveTab !== activeTab; + + // activeTab이 변경되지 않았고, 마지막 포커스로부터 1초 이상 지났으면 + // 다른 탭에서 돌아온 것으로 간주 + const isFromOtherTab = !isTabSwitch && timeSinceLastFocus > 1000; + + if (isFromOtherTab || previousActiveTab === null) { + // 다른 탭에서 돌아온 경우 또는 처음 Stats로 온 경우: 날짜를 오늘로 설정 + const today = new Date(); + setSelectedDate(today); + console.log('[Stats] 다른 탭에서 돌아옴 또는 처음 진입 - 날짜를 오늘로 설정'); + } else { + // Stats 내부에서 탭 전환한 경우: 날짜 유지 + console.log('[Stats] Stats 내부에서 탭 전환 - 날짜 유지'); + } + + // 현재 activeTab을 이전 값으로 저장 + previousActiveTabRef.current = activeTab; + lastFocusTimeRef.current = now; + }, [activeTab, setSelectedDate]) + ); + const tabs = ['운동기록', '식단기록']; const renderTabContent = () => { diff --git a/src/services/homeAPI.ts b/src/services/homeAPI.ts index c8250bf..d44b0ce 100644 --- a/src/services/homeAPI.ts +++ b/src/services/homeAPI.ts @@ -75,22 +75,24 @@ export const homeAPI = { // 응답: [{ date: "2025-11-11", exerciseRate: 0, totalCalorie: 0 }, ...] getWeeklyProgress: async (): Promise => { try { - console.log('주간 진행률 API 호출: GET /api/daily-progress/week'); + console.log('📡 [주간 진행률] API 호출: GET /api/daily-progress/week'); const response = await request(`/api/daily-progress/week`, { method: 'GET', }); // 배열로 반환 if (Array.isArray(response)) { - console.log('주간 진행률 데이터 수신:', response.length, '개'); + console.log('✅ [주간 진행률] 데이터 수신:', response.length, '개'); + // 전체 응답 데이터 로그 출력 + console.log('📊 [주간 진행률] 전체 응답 데이터:', JSON.stringify(response, null, 2)); return response; } // 예외 처리 - console.warn('주간 진행률 응답이 배열이 아닙니다:', response); + console.warn('⚠️ [주간 진행률] 응답이 배열이 아닙니다:', response); return []; } catch (error: any) { - console.error('주간 진행률 API 호출 실패:', error); + console.error('❌ [주간 진행률] API 호출 실패:', error); throw error; } }, @@ -108,6 +110,10 @@ export const homeAPI = { // 배열로 반환 if (Array.isArray(response)) { console.log('월별 진행률 데이터 수신:', response.length, '개'); + // 응답 데이터 샘플 로그 출력 + if (response.length > 0) { + console.log('월별 진행률 데이터 샘플 (첫 3개):', response.slice(0, 3)); + } return response; } diff --git a/src/services/mealAPI.ts b/src/services/mealAPI.ts index e5d2ebc..0413af9 100644 --- a/src/services/mealAPI.ts +++ b/src/services/mealAPI.ts @@ -1506,4 +1506,122 @@ export const mealAPI = { throw error; } }, + + /** + * 하루 총 섭취 칼로리 및 영양성분 조회 + * GET /food/nutrition/summary?user_id={user_id}&date={date} + * + * 사용자가 해당 날짜에 섭취한 총 칼로리 및 영양소 합계를 반환합니다. + * - Meal / MealItem 기록을 기반으로 + * - daily_nutrition_summary 테이블에 저장된 값을 조회 + * - 기록이 없으면 0으로 반환 + */ + getNutritionSummary: async (date: string): Promise<{ + calories?: number; + carbs?: number; + protein?: number; + fat?: number; + [key: string]: any; + }> => { + const user_id = await getUserId(); + + // 날짜 형식 검증 + if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) { + throw new Error('날짜 형식이 올바르지 않습니다. yyyy-MM-dd 형식을 사용해주세요.'); + } + + const token = await AsyncStorage.getItem(ACCESS_TOKEN_KEY); + const headers: HeadersInit = { + 'accept': 'application/json', + }; + + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + const url = `${AI_API_BASE_URL}/food/nutrition/summary?user_id=${encodeURIComponent(user_id)}&date=${encodeURIComponent(date)}`; + console.log(`📡 영양성분 요약 조회 요청 (날짜: ${date}): ${url}`); + + try { + const response = await fetch(url, { + method: 'GET', + headers, + }); + + if (!response.ok) { + // 404나 다른 에러인 경우 빈 객체 반환 (기록이 없으면 0으로 반환) + if (response.status === 404) { + console.log(`📊 영양성분 요약 없음 (날짜: ${date})`); + return { + calories: 0, + carbs: 0, + protein: 0, + fat: 0, + }; + } + + const errorText = await response.text(); + console.error(`❌ 영양성분 요약 조회 에러 응답:`, errorText); + + // 422 Validation Error 처리 + if (response.status === 422) { + try { + const errorData = JSON.parse(errorText); + if (errorData.detail && Array.isArray(errorData.detail)) { + const errorMessages = errorData.detail.map((err: any) => { + const field = err.loc && Array.isArray(err.loc) + ? err.loc.filter((loc: any) => typeof loc === 'string').join('.') + : 'unknown'; + return `${field}: ${err.msg || '검증 오류'}`; + }); + throw new Error(errorMessages.join(', ')); + } else if (errorData.detail && typeof errorData.detail === 'string') { + throw new Error(errorData.detail); + } + } catch (parseError) { + throw new Error('요청 파라미터가 올바르지 않습니다. user_id와 date를 확인해주세요.'); + } + } + + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + console.log(`✅ 영양성분 요약 조회 성공 (날짜: ${date}):`, data); + + // 응답 형식: { user_id, date, exists, total: { kcal, protein_g, carb_g, fat_g, ... } } + const total = data.total || {}; + + return { + calories: total.kcal || 0, + carbs: total.carb_g || 0, + protein: total.protein_g || 0, + fat: total.fat_g || 0, + fiber: total.fiber_g || 0, + sugar: total.sugar_g || 0, + sodium: total.sodium_mg || 0, + exists: data.exists || false, + ...data, // 기타 필드도 포함 + }; + } catch (error: any) { + // 네트워크 에러나 기타 에러인 경우 빈 객체 반환 + if (error.message?.includes('404') || error.message?.includes('찾을 수 없')) { + return { + calories: 0, + carbs: 0, + protein: 0, + fat: 0, + }; + } + + console.error('영양성분 요약 조회 실패:', error); + // 에러가 발생해도 빈 객체 반환 (기록이 없으면 0으로 반환) + return { + calories: 0, + carbs: 0, + protein: 0, + fat: 0, + }; + } + }, }; diff --git a/src/types/index.ts b/src/types/index.ts index cd2c86e..0ba1044 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -156,6 +156,9 @@ export interface DailyProgressWeekItem { date: string; // yyyy-MM-dd 형식 exerciseRate: number; // 운동 달성률 (0~100) totalCalorie: number; // 총 칼로리 + mealCount?: number; // 식사 횟수 + exerciseCount?: number; // 운동 종목 수 + exerciseTime?: number; // 운동 시간 (초) } // 영양 목표 타입