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; // 운동 시간 (초)
}
// 영양 목표 타입