diff --git a/src/components/ExerciseSetItem.tsx b/src/components/ExerciseSetItem.tsx index e3bfec4..1ec290f 100644 --- a/src/components/ExerciseSetItem.tsx +++ b/src/components/ExerciseSetItem.tsx @@ -18,7 +18,6 @@ interface ExerciseSetItemProps { onPressRemove: () => void; onWeightChange?: (weight: number) => void; onRepsChange?: (reps: number) => void; - onOrderChange?: (order: number) => void; } const ExerciseSetItem: React.FC = ({ @@ -31,7 +30,6 @@ const ExerciseSetItem: React.FC = ({ onPressRemove, onWeightChange, onRepsChange, - onOrderChange, }) => { const isHighlighted = isActive || isCompleted; @@ -49,36 +47,12 @@ const ExerciseSetItem: React.FC = ({ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} > - + - {/* 세트 번호 - 편집 가능 */} - {onOrderChange ? ( - - { - const num = parseInt(text) || 1; - onOrderChange(num); - }} - keyboardType="numeric" - editable={!isCompleted} - /> - - 세트 - - - ) : ( + {/* 세트 번호 */} + = ({ > {order ?? 1}세트 - )} + {/* 무게 */} {onWeightChange ? ( - { - const num = parseInt(text) || 0; - onWeightChange(num); - }} - keyboardType="numeric" - editable={!isCompleted} - /> - - kg - + + { + const num = parseFloat(text) || 0; + onWeightChange(num); + }} + keyboardType="decimal-pad" + editable={!isCompleted} + placeholder="0" + placeholderTextColor="#999" + /> + + kg + + ) : ( = ({ {/* 횟수 */} {onRepsChange ? ( - { - const num = parseInt(text) || 0; - onRepsChange(num); - }} - keyboardType="numeric" - editable={!isCompleted} - /> - - 회 - + + { + const num = parseInt(text) || 0; + onRepsChange(num); + }} + keyboardType="number-pad" + editable={!isCompleted} + placeholder="0" + placeholderTextColor="#999" + /> + + 회 + + ) : ( = ({ )} - {/* 체크박스 */} + {/* 완료 버튼 */} - {isCompleted && ( - - )} - {!isCompleted && ( - + {isCompleted ? ( + + ) : ( + )} @@ -185,26 +166,32 @@ const styles = StyleSheet.create({ container: { flexDirection: "row", alignItems: "center", - paddingVertical: 10, - paddingHorizontal: 18, - marginHorizontal: 12, - marginBottom: 8, - borderRadius: 12, + justifyContent: "space-between", + paddingVertical: 16, + paddingHorizontal: 16, + marginHorizontal: 0, + marginBottom: 12, + borderRadius: 16, position: "relative", + borderWidth: 1.5, shadowColor: "#000", shadowOffset: { width: 0, - height: 1, + height: 2, }, - shadowOpacity: 0.1, - shadowRadius: 2, + shadowOpacity: 0.08, + shadowRadius: 4, elevation: 2, + gap: 12, + width: "100%", }, containerDefault: { backgroundColor: "#FFFFFF", + borderColor: "#E8E8E8", }, containerActive: { - backgroundColor: "#E8FF8A", + backgroundColor: "#F0F9F0", + borderColor: "#4CAF50", }, deleteButton: { position: "absolute", @@ -213,67 +200,78 @@ const styles = StyleSheet.create({ zIndex: 1, }, deleteButtonCircle: { - width: 24, - height: 24, - borderRadius: 12, - backgroundColor: "#CCCCCC", + width: 28, + height: 28, + borderRadius: 14, + backgroundColor: "#FF5252", justifyContent: "center", alignItems: "center", + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.2, + shadowRadius: 3, + elevation: 3, }, - setNumber: { - fontSize: 14, - fontWeight: "400", - minWidth: 50, - marginRight: 12, - }, - orderContainer: { - flexDirection: "row", - alignItems: "center", - minWidth: 60, - marginRight: 12, - }, - orderInput: { - fontSize: 14, - fontWeight: "400", - minWidth: 30, - textAlign: "right", - padding: 0, + setNumberContainer: { + width: 60, + justifyContent: "center", + alignItems: "flex-start", }, - orderUnit: { - fontSize: 14, - fontWeight: "400", - marginLeft: 4, + setNumber: { + fontSize: 15, + fontWeight: "600", + textAlign: "left", }, weight: { - fontSize: 14, - fontWeight: "400", + fontSize: 15, + fontWeight: "600", flex: 1, - marginRight: 12, + maxWidth: "35%", + textAlign: "right", }, reps: { - fontSize: 14, - fontWeight: "400", + fontSize: 15, + fontWeight: "600", flex: 1, - marginRight: 12, + maxWidth: "35%", + textAlign: "right", }, valueContainer: { flex: 1, + maxWidth: "35%", + }, + inputWrapper: { flexDirection: "row", alignItems: "center", - marginRight: 12, + justifyContent: "flex-end", + backgroundColor: "#F8F8F8", + borderRadius: 10, + paddingHorizontal: 12, + paddingVertical: 10, + borderWidth: 1, + borderColor: "#E0E0E0", + width: "100%", }, valueInput: { - flex: 1, - fontSize: 14, - fontWeight: "400", + fontSize: 16, + fontWeight: "600", textAlign: "right", - minWidth: 40, + width: 50, padding: 0, + color: "#000000", + }, + valueInputDefault: { + color: "#000000", + }, + valueInputActive: { + color: "#4CAF50", }, unit: { fontSize: 14, - fontWeight: "400", - marginLeft: 4, + fontWeight: "600", + marginLeft: 6, + color: "#666666", + width: 24, }, textDefault: { color: "#000000", @@ -281,21 +279,18 @@ const styles = StyleSheet.create({ textActive: { color: "#000000", }, - checkbox: { - width: 32, - height: 32, - borderRadius: 8, - borderWidth: 1, + completeButton: { + width: 50, + height: 50, justifyContent: "center", alignItems: "center", + flexShrink: 0, }, - checkboxDefault: { - backgroundColor: "#FFFFFF", - borderColor: "#E0E0E0", + completeButtonDefault: { + backgroundColor: "transparent", }, - checkboxCompleted: { - backgroundColor: "#FFFFFF", - borderColor: "#000000", + completeButtonActive: { + backgroundColor: "transparent", }, }); diff --git a/src/components/modals/ExerciseModal.tsx b/src/components/modals/ExerciseModal.tsx index d749b4f..782222e 100644 --- a/src/components/modals/ExerciseModal.tsx +++ b/src/components/modals/ExerciseModal.tsx @@ -320,6 +320,7 @@ const ExerciseModal: React.FC = ({ ] ); const prefetchedInstructionUrlsRef = useRef>(new Set()); + const detailScrollViewRef = useRef(null); // 이미지 로드 실패 추적 (URL을 키로 사용) const [failedImageUrls, setFailedImageUrls] = useState>(new Set()); // 운동별 피드백 상태 관리 (운동 이름을 키로 사용) @@ -633,6 +634,10 @@ const ExerciseModal: React.FC = ({ (direction: "prev" | "next") => { cacheCurrentExerciseSets(); persistCurrentExerciseState(); + // 다음 버튼을 눌렀을 때 스크롤을 맨 위로 + if (direction === "next") { + detailScrollViewRef.current?.scrollTo({ y: 0, animated: true }); + } onSequenceNavigate?.(direction); }, [cacheCurrentExerciseSets, persistCurrentExerciseState, onSequenceNavigate] @@ -1758,7 +1763,7 @@ const getExerciseDisplayName = React.useCallback( }} style={styles.backBtnTop} > - + {getExerciseDisplayName( @@ -1766,34 +1771,6 @@ const getExerciseDisplayName = React.useCallback( )} - {!isCompleted && ( - setShowExerciseListModal(true)} - activeOpacity={0.85} - > - - - )} - {mode !== "edit" && !isCompleted && ( - - - - {formatWorkoutTimer(workoutTimerSeconds)} - - - )} @@ -1812,42 +1789,15 @@ const getExerciseDisplayName = React.useCallback( - {!isCompleted && ( - setShowExerciseListModal(true)} - activeOpacity={0.85} - > - - - )} - {mode !== "edit" && !isCompleted && ( - - - - {formatWorkoutTimer(workoutTimerSeconds)} - - - )} - + )} - + )} {/* 오른쪽 버튼: 운동이 2개 이상이고, 현재가 마지막이 아니면 표시 */} @@ -1953,7 +1903,7 @@ const getExerciseDisplayName = React.useCallback( } }} > - + )} @@ -1970,7 +1920,7 @@ const getExerciseDisplayName = React.useCallback( {showInstructionsSection && ( @@ -2055,16 +2005,6 @@ const getExerciseDisplayName = React.useCallback( - - - - - - {sets.map((set) => ( - handleOrderChange(set.id, order) - } onWeightChange={(weight) => handleSetChange(set.id, "weight", weight) } @@ -2091,6 +2028,16 @@ const getExerciseDisplayName = React.useCallback( /> ))} + + + + 세트 추가 + + @@ -2156,82 +2103,90 @@ const getExerciseDisplayName = React.useCallback( }); }; + const hasFeedback = currentFeedback.feedback !== null; + return ( - 이 운동 어땠나요? - - handleIntensityClick("heavy")} - > - - 무거워요 - - - handleIntensityClick("light")} - > - - 가벼워요 - - - handleFeedbackClick("like")} - > - + + 난이도는 어땠나요? + + handleIntensityClick("heavy")} + > + + 무거워요 + + + handleIntensityClick("light")} + > + + 가벼워요 + + + + + + )} + + {/* 이 운동 어땠나요 섹션 */} + + 이 운동 어땠나요? + + handleFeedbackClick("like")} > - 좋아요 - - - handleFeedbackClick("dislike")} - > - + + handleFeedbackClick("dislike")} > - 싫어요 - - + + + ); @@ -2246,27 +2201,34 @@ const getExerciseDisplayName = React.useCallback( onPress={() => handleSequenceNavigatePress("prev")} disabled={!hasPrevSequence} > + - 이전 운동 + 이전 {!isCompleted && ( - 타이머 - + + {formatWorkoutTimer(workoutTimerSeconds)} @@ -2285,28 +2247,27 @@ const getExerciseDisplayName = React.useCallback( !canNextExercise && styles.sequenceControlTextDisabled, ]} > - 다음 운동 + 다음 + )} - - - 운동 끝내기 - - + + 운동 끝내기 + + + )} )} @@ -2322,10 +2283,10 @@ const getExerciseDisplayName = React.useCallback( > {!fullScreen && ( - + - 종목 추가 + 종목 추가 @@ -2630,16 +2591,6 @@ const getExerciseDisplayName = React.useCallback( - - - - - - {sets.map((set) => ( - handleOrderChange(set.id, order) - } onWeightChange={(weight) => handleSetChange(set.id, "weight", weight) } @@ -2666,6 +2614,16 @@ const getExerciseDisplayName = React.useCallback( /> ))} + + + + 세트 추가 + + @@ -2676,23 +2634,16 @@ const getExerciseDisplayName = React.useCallback( allSetsCompleted && !isCompleted && styles.footerWithFeedback, ]} > - - - 운동 끝내기 - - + + 운동 끝내기 + + + )} {allSetsCompleted && !isCompleted && (() => { const currentExerciseName = getExerciseDisplayName( selectedExercise || exerciseData || { name: "" } @@ -2746,82 +2697,90 @@ const getExerciseDisplayName = React.useCallback( }); }; + const hasFeedback = currentFeedback.feedback !== null; + return ( - 이 운동 어땠나요? - - handleIntensityClick("heavy")} - > - - 무거워요 - - - handleIntensityClick("light")} - > - - 가벼워요 - - - handleFeedbackClick("like")} - > - + + 난이도는 어땠나요? + + handleIntensityClick("heavy")} + > + + 무거워요 + + + handleIntensityClick("light")} + > + + 가벼워요 + + + + + + )} + + {/* 이 운동 어땠나요 섹션 */} + + 이 운동 어땠나요? + + handleFeedbackClick("like")} > - 좋아요 - - - handleFeedbackClick("dislike")} - > - + + handleFeedbackClick("dislike")} > - 싫어요 - - + + + ); @@ -2950,21 +2909,21 @@ const styles = StyleSheet.create({ backgroundColor: "#2a2a2a", borderTopLeftRadius: 20, borderTopRightRadius: 20, - height: "70%", // 고정 비율 높이 상향 + height: "85%", // 고정 비율 높이 상향 minHeight: 0, overflow: "hidden", }, fullScreenContainer: { flex: 1, - backgroundColor: "#0c0c0c", + backgroundColor: "#ffffff", }, fullScreenContent: { flex: 1, - backgroundColor: "#0c0c0c", + backgroundColor: "#ffffff", }, fullScreenHeader: { paddingHorizontal: 20, - backgroundColor: "#0c0c0c", + backgroundColor: "#ffffff", paddingTop: 16, paddingBottom: 2, }, @@ -2992,7 +2951,7 @@ const styles = StyleSheet.create({ flex: 1, fontSize: 18, fontWeight: "600", - color: "#ffffff", + color: "#000000", textAlign: "center", }, detailHeaderActions: { @@ -3022,7 +2981,7 @@ const styles = StyleSheet.create({ }, exerciseDetailModal: { flex: 1, - backgroundColor: "#0c0c0c", + backgroundColor: "#ffffff", }, detailKeyboardAvoider: { flex: 1, @@ -3033,7 +2992,15 @@ const styles = StyleSheet.create({ alignItems: "center", padding: 20, borderBottomWidth: 1, - borderBottomColor: "#404040", + borderBottomColor: "#e8e8e8", + backgroundColor: "#ffffff", + }, + addExerciseModalHeader: { + backgroundColor: "#252525", + borderBottomColor: "#3a3a3a", + }, + addExerciseModalTitle: { + color: "#ffffff", }, headerLeft: { width: 52, @@ -3052,14 +3019,14 @@ const styles = StyleSheet.create({ width: 36, height: 36, borderRadius: 18, - backgroundColor: "rgba(255,255,255,0.14)", + backgroundColor: "#f5f5f5", justifyContent: "center", alignItems: "center", }, modalTitle: { fontSize: 18, fontWeight: "600", - color: "#ffffff", + color: "#000000", flex: 1, textAlign: "center", }, @@ -3084,22 +3051,48 @@ const styles = StyleSheet.create({ timerBadge: { flexDirection: "row", alignItems: "center", - gap: 4, - backgroundColor: "rgba(255,255,255,0.1)", - borderRadius: 18, - paddingHorizontal: 10, - paddingVertical: 6, - borderWidth: 1, - borderColor: "rgba(255,255,255,0.16)", + gap: 6, + backgroundColor: "#000000", + borderRadius: 20, + paddingHorizontal: 14, + paddingVertical: 8, + borderWidth: 0, + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, }, timerBadgePaused: { - backgroundColor: "rgba(255,255,255,0.04)", - borderColor: "rgba(255,255,255,0.08)", + backgroundColor: "#f5f5f5", + borderWidth: 1, + borderColor: "#e0e0e0", }, timerBadgeText: { color: "#ffffff", - fontSize: 13, - fontWeight: "600", + fontSize: 14, + fontWeight: "700", + letterSpacing: 0.5, + }, + timerBadgeTextPaused: { + color: "#666666", + }, + timerBadgeInline: { + flexDirection: "row", + alignItems: "center", + paddingHorizontal: 12, + paddingVertical: 6, + backgroundColor: "#f0f9f0", + borderRadius: 20, + minWidth: 80, + borderWidth: 1, + borderColor: "#4CAF50", + }, + timerBadgeTextInline: { + color: "#4CAF50", + fontSize: 16, + fontWeight: "700", + letterSpacing: 0.5, }, methodBtn: { paddingHorizontal: 10, @@ -3116,13 +3109,15 @@ const styles = StyleSheet.create({ marginHorizontal: 20, marginTop: 0, marginBottom: 24, - backgroundColor: "#333333", - borderRadius: 10, + backgroundColor: "#f8f8f8", + borderRadius: 12, padding: 16, + borderWidth: 1, + borderColor: "#e8e8e8", }, instructionTitle: { - fontSize: 13, - color: "#ffffff", + fontSize: 14, + color: "#000000", fontWeight: "600", marginBottom: 0, }, @@ -3133,9 +3128,9 @@ const styles = StyleSheet.create({ gap: 8, }, instructionText: { - fontSize: 12, - color: "#cccccc", - lineHeight: 17, + fontSize: 13, + color: "#666666", + lineHeight: 20, }, instructionList: { marginTop: 12, @@ -3145,8 +3140,8 @@ const styles = StyleSheet.create({ marginBottom: 16, }, instructionNumber: { - fontSize: 12, - color: "#ffffff", + fontSize: 13, + color: "#000000", fontWeight: "600", marginRight: 8, minWidth: 20, @@ -3156,13 +3151,14 @@ const styles = StyleSheet.create({ }, exerciseImageContainer: { position: "relative", - width: "100%", height: 270, marginTop: 0, marginBottom: 24, - backgroundColor: "#1a1a1a", + marginHorizontal: 20, + backgroundColor: "#f8f8f8", justifyContent: "center", alignItems: "center", + borderRadius: 16, }, exerciseImageLarge: { width: "100%", @@ -3179,87 +3175,148 @@ const styles = StyleSheet.create({ left: 12, top: "50%", marginTop: -16, - width: 32, - height: 32, - borderRadius: 16, - backgroundColor: "rgba(0, 0, 0, 0.5)", + width: 36, + height: 36, + borderRadius: 18, + backgroundColor: "rgba(255, 255, 255, 0.9)", justifyContent: "center", alignItems: "center", + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.2, + shadowRadius: 4, + elevation: 3, }, exerciseImageNavRight: { position: "absolute", right: 12, top: "50%", marginTop: -16, - width: 32, - height: 32, - borderRadius: 16, - backgroundColor: "rgba(0, 0, 0, 0.5)", + width: 36, + height: 36, + borderRadius: 18, + backgroundColor: "rgba(255, 255, 255, 0.9)", justifyContent: "center", alignItems: "center", + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.2, + shadowRadius: 4, + elevation: 3, }, loadingText: { - color: "#666666", + color: "#999999", fontSize: 14, }, addSetButtonWrapper: { alignItems: "center", - marginTop: 0, - marginBottom: 12, + marginTop: 12, + marginBottom: 16, + paddingHorizontal: 0, }, - addSetCircleButton: { - width: 32, - height: 32, - borderRadius: 16, - backgroundColor: "#e3ff7c", - justifyContent: "center", + addSetButton: { + flexDirection: "row", alignItems: "center", - shadowColor: "#000", - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.2, - shadowRadius: 4, - elevation: 3, + justifyContent: "center", + backgroundColor: "#f0f9f0", + borderRadius: 12, + paddingVertical: 14, + paddingHorizontal: 20, + borderWidth: 1.5, + borderColor: "#4CAF50", + borderStyle: "dashed", + width: "100%", + gap: 8, + }, + addSetButtonText: { + color: "#4CAF50", + fontSize: 16, + fontWeight: "700", }, feedbackSection: { marginTop: 0, marginHorizontal: 0, marginBottom: 12, }, - feedbackTitle: { - fontSize: 18, - fontWeight: "600", - color: "#ffffff", - marginBottom: 16, - textAlign: "center", + intensitySection: { + marginBottom: 24, }, - feedbackButtonsRow: { + intensityTitleRow: { flexDirection: "row", - flexWrap: "wrap", + alignItems: "center", justifyContent: "space-between", + gap: 16, }, - feedbackButton: { - width: "48%", - backgroundColor: "#ffffff", - borderWidth: 1, - borderColor: "#000000", - borderRadius: 8, - paddingVertical: 14, + intensityTitle: { + fontSize: 16, + fontWeight: "600", + color: "#000000", + marginBottom: 0, + flex: 1, + }, + intensityButtonsRow: { + flexDirection: "row", + alignItems: "center", + gap: 12, + flexWrap: "nowrap", + }, + intensityButton: { + backgroundColor: "#f8f8f8", + borderWidth: 1.5, + borderColor: "#e0e0e0", + borderRadius: 10, + paddingVertical: 10, + paddingHorizontal: 16, alignItems: "center", justifyContent: "center", - marginBottom: 12, + minWidth: 80, }, - feedbackButtonSelected: { - backgroundColor: "#d6ff4b", - borderColor: "#d6ff4b", + intensityButtonSelected: { + backgroundColor: "#4CAF50", + borderColor: "#4CAF50", }, - feedbackButtonText: { - color: "#000000", - fontSize: 14, - fontWeight: "500", + intensityButtonText: { + color: "#666666", + fontSize: 12, + fontWeight: "600", }, - feedbackButtonTextSelected: { - color: "#000000", + intensityButtonTextSelected: { + color: "#ffffff", + fontSize: 12, + fontWeight: "700", + }, + feedbackTitleRow: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + gap: 16, + }, + feedbackTitle: { + fontSize: 16, fontWeight: "600", + color: "#000000", + marginBottom: 0, + flex: 1, + }, + feedbackButtonsRow: { + flexDirection: "row", + alignItems: "center", + gap: 12, + flexWrap: "nowrap", + }, + feedbackIconButton: { + width: 44, + height: 44, + borderRadius: 22, + backgroundColor: "#f8f8f8", + borderWidth: 2, + borderColor: "#e0e0e0", + alignItems: "center", + justifyContent: "center", + }, + feedbackIconButtonSelected: { + backgroundColor: "#4CAF50", + borderColor: "#4CAF50", }, exerciseListOverlay: { flex: 1, @@ -3388,22 +3445,33 @@ const styles = StyleSheet.create({ borderBottomColor: "#f0f0f0", }, endWorkoutBtn: { - backgroundColor: "#404040", - paddingVertical: 10, - borderRadius: 12, + backgroundColor: "#000000", + paddingVertical: 18, + borderRadius: 14, alignItems: "center", + justifyContent: "center", + width: "100%", + shadowColor: "#000", + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.15, + shadowRadius: 8, + elevation: 5, + marginTop: 12, }, endWorkoutBtnDisabled: { - backgroundColor: "#2a2a2a", - opacity: 0.5, + backgroundColor: "#e8e8e8", + opacity: 1, + shadowOpacity: 0, + elevation: 0, }, endWorkoutBtnText: { color: "#ffffff", - fontSize: 16, - fontWeight: "600", + fontSize: 17, + fontWeight: "700", + letterSpacing: 0.3, }, endWorkoutBtnTextDisabled: { - color: "#888888", + color: "#999999", }, instructionScroll: { maxHeight: 260, @@ -3669,6 +3737,7 @@ const styles = StyleSheet.create({ paddingBottom: 0, flex: 1, minHeight: 0, + width: "100%", }, setsHeader: { flexDirection: "row", @@ -3696,6 +3765,7 @@ const styles = StyleSheet.create({ marginTop: 0, paddingTop: 0, paddingBottom: 12, + width: "100%", }, setControlBtn: { width: 32, @@ -3831,7 +3901,9 @@ const styles = StyleSheet.create({ paddingHorizontal: 20, paddingBottom: 0, paddingTop: 16, - backgroundColor: "#2a2a2a", + backgroundColor: "#ffffff", + borderTopWidth: 1, + borderTopColor: "#e8e8e8", }, footerExtended: { flexDirection: "column", @@ -3863,23 +3935,25 @@ const styles = StyleSheet.create({ commentLabel: { fontSize: 14, fontWeight: "600", - color: "#ffffff", + color: "#000000", }, commentInputWrapper: { - backgroundColor: "#1f1f1f", + backgroundColor: "#f8f8f8", borderRadius: 12, padding: 16, gap: 8, + borderWidth: 1, + borderColor: "#e8e8e8", }, commentInput: { minHeight: 60, - color: "#ffffff", + color: "#000000", fontSize: 14, lineHeight: 20, }, commentCounter: { fontSize: 12, - color: "#888888", + color: "#999999", textAlign: "right", }, sequenceControlContainer: { @@ -3896,39 +3970,45 @@ const styles = StyleSheet.create({ sequenceControlButton: { flex: 1, borderRadius: 12, - backgroundColor: "#272727", - paddingVertical: 8, + backgroundColor: "#f8f8f8", + paddingVertical: 14, alignItems: "center", justifyContent: "center", + borderWidth: 1, + borderColor: "#e8e8e8", + flexDirection: "row", + minHeight: 48, }, sequenceControlButtonDisabled: { - backgroundColor: "#1a1a1a", - opacity: 0.4, + backgroundColor: "#f5f5f5", + opacity: 0.5, + borderColor: "#e0e0e0", }, sequenceControlText: { - color: "#ffffff", - fontSize: 14, + color: "#000000", + fontSize: 15, fontWeight: "600", }, sequenceControlTextDisabled: { - color: "#888888", + color: "#cccccc", }, sequenceTimerButton: { - gap: 6, - paddingVertical: 10, - }, - sequenceTimerLabel: { - color: "#bbbbbb", - fontSize: 12, - fontWeight: "500", + flex: 1, + borderRadius: 12, + backgroundColor: "#f0f9f0", + paddingVertical: 14, + alignItems: "center", + justifyContent: "center", + borderWidth: 1.5, + borderColor: "#4CAF50", + flexDirection: "row", + minHeight: 48, }, - sequenceTimerValue: { - color: "#ffffff", + sequenceTimerText: { + color: "#4CAF50", fontSize: 16, fontWeight: "700", - }, - sequenceTimerValuePaused: { - color: "#ffb84d", + letterSpacing: 0.5, }, commentSendButton: { backgroundColor: "#e3ff7c", diff --git a/src/screens/analysis/CalendarScreen.tsx b/src/screens/analysis/CalendarScreen.tsx index 2576cf5..f24455f 100644 --- a/src/screens/analysis/CalendarScreen.tsx +++ b/src/screens/analysis/CalendarScreen.tsx @@ -11,8 +11,9 @@ import { Ionicons as Icon } from '@expo/vector-icons'; import {colors} from '../../theme/colors'; import {useDate} from '../../contexts/DateContext'; import {fetchWeeklyProgress, fetchMonthlyProgress} from '../../utils/exerciseApi'; -import {mealAPI} from '../../services'; +import {mealAPI, homeAPI} from '../../services'; import type {DailyProgressWeekItem, NutritionGoal, DailyMealsResponse} from '../../types'; +import {eventBus} from '../../utils/eventBus'; const CalendarScreen = ({navigation}: any) => { const [monthBase, setMonthBase] = useState(new Date()); @@ -35,7 +36,82 @@ const CalendarScreen = ({navigation}: any) => { const loadWeeklyProgress = async () => { try { const data = await fetchWeeklyProgress(); - setWeeklyProgress(Array.isArray(data) ? 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); + }); + + // 각 날짜에 대해 영양성분 요약 조회 (병렬 처리) + const nutritionPromises = weekDates.map(async (date) => { + try { + const summary = await mealAPI.getNutritionSummary(date); + return { date, calories: summary.calories || 0 }; + } catch (error) { + console.error(`영양성분 조회 실패 (${date}):`, error); + return { date, calories: 0 }; + } + }); + + // 각 날짜에 대해 운동 시간 조회 (병렬 처리) + const workoutTimePromises = weekDates.map(async (date) => { + try { + const progress = await homeAPI.getTodayProgress(date); + return { date, totalSeconds: progress.totalExerciseSeconds || 0 }; + } catch (error) { + console.error(`운동 시간 조회 실패 (${date}):`, error); + return { date, totalSeconds: 0 }; + } + }); + + const [nutritionResults, workoutTimeResults] = await Promise.all([ + Promise.all(nutritionPromises), + Promise.all(workoutTimePromises), + ]); + + // 기존 데이터와 병합 (칼로리 및 운동 시간 데이터 업데이트) + let updatedData: DailyProgressWeekItem[] = []; + + if (Array.isArray(data) && data.length > 0) { + updatedData = weekDates.map((date) => { + const existingItem = data.find((item) => item.date === date); + const nutritionItem = nutritionResults.find((item) => item.date === date); + const workoutItem = workoutTimeResults.find((item) => item.date === date); + + return { + date, + exerciseRate: existingItem?.exerciseRate ?? 0, + totalCalorie: nutritionItem?.calories ?? existingItem?.totalCalorie ?? 0, + // 운동 시간은 백엔드에서 exerciseRate로 계산되지만, 최신 데이터를 위해 재조회 + }; + }); + } else { + updatedData = weekDates.map((date) => { + const nutritionItem = nutritionResults.find((item) => item.date === date); + return { + date, + exerciseRate: 0, + totalCalorie: nutritionItem?.calories ?? 0, + }; + }); + } + + setWeeklyProgress(updatedData); } catch (e) { console.error('주간 진행률 로드 실패:', e); setWeeklyProgress([]); @@ -47,7 +123,71 @@ const CalendarScreen = ({navigation}: any) => { try { const yearMonth = `${year}-${String(month + 1).padStart(2, '0')}`; const data = await fetchMonthlyProgress(yearMonth); - setMonthlyProgress(Array.isArray(data) ? data : []); + + // 해당 월의 모든 날짜 계산 + 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); + }); + + // 각 날짜에 대해 영양성분 요약 조회 (병렬 처리) + const nutritionPromises = monthDates.map(async (date) => { + try { + const summary = await mealAPI.getNutritionSummary(date); + return { date, calories: summary.calories || 0 }; + } catch (error) { + console.error(`영양성분 조회 실패 (${date}):`, error); + return { date, calories: 0 }; + } + }); + + // 각 날짜에 대해 운동 시간 조회 (병렬 처리) + const workoutTimePromises = monthDates.map(async (date) => { + try { + const progress = await homeAPI.getTodayProgress(date); + return { date, totalSeconds: progress.totalExerciseSeconds || 0 }; + } catch (error) { + console.error(`운동 시간 조회 실패 (${date}):`, error); + return { date, totalSeconds: 0 }; + } + }); + + const [nutritionResults, workoutTimeResults] = await Promise.all([ + Promise.all(nutritionPromises), + Promise.all(workoutTimePromises), + ]); + + // 기존 데이터와 병합 (칼로리 및 운동 시간 데이터 업데이트) + let updatedData: DailyProgressWeekItem[] = []; + + if (Array.isArray(data) && data.length > 0) { + updatedData = monthDates.map((date) => { + const existingItem = data.find((item) => item.date === date); + const nutritionItem = nutritionResults.find((item) => item.date === date); + const workoutItem = workoutTimeResults.find((item) => item.date === date); + + return { + date, + exerciseRate: existingItem?.exerciseRate ?? 0, + totalCalorie: nutritionItem?.calories ?? existingItem?.totalCalorie ?? 0, + // 운동 시간은 백엔드에서 exerciseRate로 계산되지만, 최신 데이터를 위해 재조회 + }; + }); + } else { + updatedData = monthDates.map((date) => { + const nutritionItem = nutritionResults.find((item) => item.date === date); + return { + date, + exerciseRate: 0, + totalCalorie: nutritionItem?.calories ?? 0, + }; + }); + } + + setMonthlyProgress(updatedData); } catch (e) { console.error('월별 진행률 로드 실패:', e); setMonthlyProgress([]); @@ -120,6 +260,49 @@ const CalendarScreen = ({navigation}: any) => { return unsubscribe; }, [navigation]); + // 식사 삭제 이벤트 리스너 + useEffect(() => { + const unsubscribe = eventBus.on("mealDeleted", () => { + console.log("[CALENDAR] 식사 삭제 이벤트 수신, 캘린더 데이터 새로고침"); + // 주간/월간 진행률과 오늘 식단 데이터 새로고침 + loadWeeklyProgress(); + loadMonthlyProgress(monthBase.getFullYear(), monthBase.getMonth()); + fetchTodayMeals(); + }); + + return () => { + unsubscribe?.(); + }; + }, [monthBase]); + + // 운동 저장/삭제 이벤트 리스너 + useEffect(() => { + const unsubscribeSaved = eventBus.on("workoutSessionSaved", () => { + console.log("[CALENDAR] 운동 저장 이벤트 수신, 캘린더 데이터 새로고침"); + // 주간/월간 진행률 새로고침 (운동 시간 업데이트) + // 약간의 지연을 두어 백엔드 데이터 업데이트 대기 + setTimeout(() => { + loadWeeklyProgress(); + loadMonthlyProgress(monthBase.getFullYear(), monthBase.getMonth()); + }, 500); + }); + + const unsubscribeDeleted = eventBus.on("workoutSessionDeleted", () => { + console.log("[CALENDAR] 운동 삭제 이벤트 수신, 캘린더 데이터 새로고침"); + // 주간/월간 진행률 새로고침 (운동 시간 업데이트) + // 약간의 지연을 두어 백엔드 데이터 업데이트 대기 + setTimeout(() => { + loadWeeklyProgress(); + loadMonthlyProgress(monthBase.getFullYear(), monthBase.getMonth()); + }, 500); + }); + + return () => { + unsubscribeSaved?.(); + unsubscribeDeleted?.(); + }; + }, [monthBase]); + // 식단 데이터 const meals = [ { diff --git a/src/screens/auth/KakaoOnboardingScreen.tsx b/src/screens/auth/KakaoOnboardingScreen.tsx index 590a1c1..3069d1a 100644 --- a/src/screens/auth/KakaoOnboardingScreen.tsx +++ b/src/screens/auth/KakaoOnboardingScreen.tsx @@ -14,7 +14,7 @@ import { SafeAreaView, } from "react-native"; import { Picker } from "@react-native-picker/picker"; -import { authAPI } from "../../services"; +import { authAPI, mealAPI } from "../../services"; const healthGoalOptions = [ { label: "벌크업", value: "BULK" }, @@ -184,6 +184,24 @@ const KakaoOnboardingScreen = ({ navigation }: any) => { const response = await authAPI.submitOnboarding(onboardingData); + // 기본 칼로리 목표 2000으로 설정 + try { + const today = new Date(); + const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; + await mealAPI.setNutritionGoal({ + targetCalories: 2000, + targetCarbs: 0, + targetProtein: 0, + targetFat: 0, + goalType: 'MANUAL', + date: todayStr, + }); + console.log('✅ 기본 칼로리 목표 2000 설정 완료'); + } catch (goalError) { + console.error('기본 칼로리 목표 설정 실패:', goalError); + // 칼로리 목표 설정 실패해도 온보딩은 성공으로 처리 + } + setLoading(false); // 200 응답 (온보딩 완료) → Alert 없이 바로 홈으로 이동 diff --git a/src/screens/auth/SignupScreen.tsx b/src/screens/auth/SignupScreen.tsx index c06b231..fd52cca 100644 --- a/src/screens/auth/SignupScreen.tsx +++ b/src/screens/auth/SignupScreen.tsx @@ -14,7 +14,7 @@ import { Linking, } from 'react-native'; import {Picker} from '@react-native-picker/picker'; -import {authAPI} from '../../services'; +import {authAPI, mealAPI} from '../../services'; const SignupScreen = ({navigation}: any) => { // 회원가입 단계 관리 (1~5단계) @@ -374,6 +374,24 @@ const SignupScreen = ({navigation}: any) => { const response = await authAPI.signup(signupData); if (response.success) { + // 기본 칼로리 목표 2000으로 설정 + try { + const today = new Date(); + const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; + await mealAPI.setNutritionGoal({ + targetCalories: 2000, + targetCarbs: 0, + targetProtein: 0, + targetFat: 0, + goalType: 'MANUAL', + date: todayStr, + }); + console.log('✅ 기본 칼로리 목표 2000 설정 완료'); + } catch (goalError) { + console.error('기본 칼로리 목표 설정 실패:', goalError); + // 칼로리 목표 설정 실패해도 회원가입은 성공으로 처리 + } + setShowCompleteScreen(true); } else { const errorMessage = response.message || '회원가입에 실패했습니다'; diff --git a/src/screens/chatbot/ChatbotScreen.tsx b/src/screens/chatbot/ChatbotScreen.tsx index 1bec95e..86b0fd8 100644 --- a/src/screens/chatbot/ChatbotScreen.tsx +++ b/src/screens/chatbot/ChatbotScreen.tsx @@ -1,5 +1,5 @@ // src/screens/chatbot/ChatbotScreen.tsx -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect } from "react"; import { View, Text, @@ -11,13 +11,11 @@ import { Platform, Alert, ActivityIndicator, - Animated, } from "react-native"; import { SafeAreaView, useSafeAreaInsets, } from "react-native-safe-area-context"; -import { LinearGradient } from "expo-linear-gradient"; import { Ionicons as Icon } from "@expo/vector-icons"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { chatAPI, ChatHistoryItem } from "../../services/chatAPI"; @@ -436,55 +434,32 @@ const ChatbotScreen = ({ navigation }: any) => { - - 🤖 - + 🤖 handleQuickSelect("exercise")} - activeOpacity={0.7} > - - 🏋️ - 운동 추천 - + 🏋️ + 운동 추천 handleQuickSelect("food")} - activeOpacity={0.7} > - - 🍗 - 식단 추천 - + 🍗 + 식단 추천 handleQuickSelect("plan")} - activeOpacity={0.7} > - - 📅 - 계획 수립 - + 📅 + 계획 수립 @@ -555,48 +530,26 @@ const ChatbotScreen = ({ navigation }: any) => { - {msg.type === "bot" && ( - - 🤖 - - )} - {msg.type === "user" ? ( - - {msg.text} - - ) : ( - - {msg.text} - - )} - {msg.type === "user" && ( - - - - )} + + {msg.text} + ))} {isLoading && ( - - - 🤖 - - - - - - - - + + ... )} @@ -608,39 +561,20 @@ const ChatbotScreen = ({ navigation }: any) => { 0 ? Math.max(insets.bottom, 4) : 4) - }, + { paddingBottom: insets.bottom > 0 ? Math.max(insets.bottom, 4) : 4 }, ]} > - - - - {isLoading ? ( - - ) : ( - - )} - - + + + + @@ -712,15 +646,13 @@ const styles = StyleSheet.create({ }, title: { fontSize: 32, - fontWeight: "700", + fontWeight: "bold", color: NEW_COLORS.text, marginBottom: 8, - letterSpacing: -0.5, }, subtitle: { fontSize: 18, color: NEW_COLORS.text_secondary, - fontWeight: "400", }, settingsButton: { flexDirection: "row", @@ -778,19 +710,9 @@ const styles = StyleSheet.create({ botImageContainer: { alignItems: "center", marginVertical: 40, - padding: 20, - }, - botImageGradient: { - width: 140, - height: 140, - borderRadius: 70, - justifyContent: "center", - alignItems: "center", - borderWidth: 2, - borderColor: NEW_COLORS.accent, }, botEmoji: { - fontSize: 80, + fontSize: 120, }, quickActions: { flexDirection: "row", @@ -799,29 +721,24 @@ const styles = StyleSheet.create({ }, actionBtn: { flex: 1, - borderRadius: 20, - overflow: "hidden", - shadowColor: NEW_COLORS.accent, - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.3, - shadowRadius: 8, - elevation: 6, - }, - actionBtnGradient: { + backgroundColor: NEW_COLORS.card_bg, padding: 20, + borderRadius: 20, alignItems: "center", - justifyContent: "center", - minHeight: 100, + shadowColor: "#000000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, }, actionIcon: { - fontSize: 36, + fontSize: 32, marginBottom: 8, }, actionText: { fontSize: 14, - fontWeight: "700", - color: "#000000", - letterSpacing: 0.3, + fontWeight: "600", + color: NEW_COLORS.text, }, historyButton: { flexDirection: "row", @@ -835,11 +752,6 @@ const styles = StyleSheet.create({ borderRadius: 24, borderWidth: 1, borderColor: NEW_COLORS.separator, - shadowColor: "#000", - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 2, }, historyButtonText: { fontSize: 15, @@ -890,146 +802,61 @@ const styles = StyleSheet.create({ messagesContainer: { paddingBottom: 10, }, - messageWrapper: { - flexDirection: "row", - alignItems: "flex-end", - marginBottom: 16, - gap: 8, - }, - userMessageWrapper: { - justifyContent: "flex-end", - }, - botMessageWrapper: { - justifyContent: "flex-start", - }, - botAvatar: { - width: 36, - height: 36, - borderRadius: 18, - backgroundColor: NEW_COLORS.card_bg, - justifyContent: "center", - alignItems: "center", - borderWidth: 2, - borderColor: NEW_COLORS.accent, - }, - botAvatarEmoji: { - fontSize: 20, - }, - userAvatar: { - width: 36, - height: 36, - borderRadius: 18, - backgroundColor: NEW_COLORS.accent, - justifyContent: "center", - alignItems: "center", - }, message: { - maxWidth: "75%", - paddingHorizontal: 16, - paddingVertical: 12, + maxWidth: "80%", + padding: 12, borderRadius: 20, + marginBottom: 12, }, userMessage: { - borderBottomRightRadius: 4, - shadowColor: NEW_COLORS.accent, - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.3, - shadowRadius: 8, - elevation: 4, + alignSelf: "flex-end", + backgroundColor: NEW_COLORS.accent, }, botMessage: { + alignSelf: "flex-start", backgroundColor: NEW_COLORS.card_bg, - borderBottomLeftRadius: 4, - borderWidth: 1, - borderColor: NEW_COLORS.separator, - shadowColor: "#000", - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 2, - }, - loadingMessage: { - paddingVertical: 16, }, userMessageText: { color: "#000000", - fontSize: 15, - lineHeight: 22, - fontWeight: "500", + fontSize: 16, }, botMessageText: { color: NEW_COLORS.text, - fontSize: 15, - lineHeight: 22, - fontWeight: "400", - }, - typingIndicator: { - flexDirection: "row", - alignItems: "center", - gap: 4, + fontSize: 16, }, - dot: { - width: 8, - height: 8, - borderRadius: 4, - backgroundColor: NEW_COLORS.text_secondary, - }, - dot1: { - opacity: 0.4, - }, - dot2: { - opacity: 0.6, - }, - dot3: { - opacity: 0.8, + loadingText: { + color: NEW_COLORS.text_secondary, + fontSize: 16, }, chatinputContainer: { - paddingHorizontal: 16, - paddingTop: 4, - backgroundColor: NEW_COLORS.background, - borderTopWidth: 1, - borderTopColor: NEW_COLORS.separator, - }, - inputWrapper: { flexDirection: "row", - alignItems: "flex-end", - gap: 10, + paddingHorizontal: 10, + paddingTop: 8, backgroundColor: NEW_COLORS.card_bg, - borderRadius: 28, - paddingHorizontal: 4, - paddingVertical: 4, - borderWidth: 1, - borderColor: NEW_COLORS.separator, + borderTopWidth: 1, + borderTopColor: NEW_COLORS.separator, + gap: 12, }, messageInput: { flex: 1, - backgroundColor: "transparent", + backgroundColor: NEW_COLORS.background, borderRadius: 24, - paddingHorizontal: 18, + paddingHorizontal: 20, paddingVertical: 12, - fontSize: 15, + fontSize: 16, color: NEW_COLORS.text, - maxHeight: 100, - minHeight: 44, }, sendBtn: { - width: 44, - height: 44, - borderRadius: 22, + width: 48, + height: 48, + borderRadius: 24, backgroundColor: NEW_COLORS.accent, justifyContent: "center", alignItems: "center", - shadowColor: NEW_COLORS.accent, - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.4, - shadowRadius: 6, - elevation: 5, }, - sendBtnDisabled: { - backgroundColor: NEW_COLORS.separator, - opacity: 0.5, - shadowOpacity: 0, - elevation: 0, + sendIcon: { + color: "#000000", + fontSize: 20, }, }); diff --git a/src/screens/diet/DietScreen.tsx b/src/screens/diet/DietScreen.tsx index 74d9dd5..f4b0d28 100644 --- a/src/screens/diet/DietScreen.tsx +++ b/src/screens/diet/DietScreen.tsx @@ -15,6 +15,7 @@ import {colors} from '../../theme/colors'; import {useDate} from '../../contexts/DateContext'; import {mealAPI, recommendedMealAPI, homeAPI} from '../../services'; import {useFocusEffect} from '@react-navigation/native'; +import {eventBus} from '../../utils/eventBus'; // 진행률 API 호출 제거 // import {fetchWeeklyProgress, fetchMonthlyProgress} from '../../utils/exerciseApi'; import type {DailyMealsResponse, DailyMeal, NutritionGoal, DailyProgressWeekItem, AddMealRequest} from '../../types'; @@ -458,9 +459,47 @@ const DietScreen = ({navigation, route}: any) => { await mealAPI.deleteMeal(mealId); Alert.alert('성공', '식사가 삭제되었습니다.'); + const dateStr = formatDateToString(dateToFetch); + // 삭제 후 데이터 새로고침 await fetchDailyMeals(dateToFetch); await loadRecommendedMealsForDate(dateToFetch); + + // 달력 칼로리 데이터 새로고침 + 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); + }); + await 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); + }); + await loadCalendarCalories(weekDates); + } + + // eventBus로 식사 삭제 이벤트 발생 (캘린더와 홈 화면 새로고침용) + eventBus.emit('mealDeleted', { date: dateStr, mealId }); } catch (error: any) { console.error('식사 삭제 실패:', error); let errorMessage = '식사 삭제에 실패했습니다.'; diff --git a/src/screens/exercise/ExerciseScreen.tsx b/src/screens/exercise/ExerciseScreen.tsx index 8818f7c..23b6446 100644 --- a/src/screens/exercise/ExerciseScreen.tsx +++ b/src/screens/exercise/ExerciseScreen.tsx @@ -2125,6 +2125,16 @@ const ExerciseScreen = ({ navigation }: any) => { loadGoalData(); loadTodayProgress(); // 오늘 진행률 로드 (게이지 업데이트) + // eventBus로 운동 저장 이벤트 발생 (캘린더 새로고침용) + const workoutDate = selectedDate + ? `${selectedDate.getFullYear()}-${String(selectedDate.getMonth() + 1).padStart(2, '0')}-${String(selectedDate.getDate()).padStart(2, '0')}` + : null; + eventBus.emit("workoutSessionSaved", { + sessionId: response.sessionId, + exerciseName: trimmedTitle, + workoutDate: workoutDate, + }); + // 운동 제목 저장 후 주간 진행률을 다시 가져와서 게이지 업데이트 // 서버에서 exerciseRate 계산에 시간이 걸릴 수 있으므로 여러 번 재시도 const retryLoadProgress = async ( @@ -3579,6 +3589,12 @@ const ExerciseScreen = ({ navigation }: any) => { style: "destructive", onPress: async () => { try { + // 삭제할 운동 정보 저장 (이벤트 발생용) + const deletedActivity = allActivities.find((activity) => activity.id === workoutId); + const workoutDate = selectedDate + ? `${selectedDate.getFullYear()}-${String(selectedDate.getMonth() + 1).padStart(2, '0')}-${String(selectedDate.getDate()).padStart(2, '0')}` + : null; + // 서버 API 호출 (sessionId가 있으면) if (sessionId) { await deleteWorkoutSession(sessionId); @@ -3589,6 +3605,13 @@ const ExerciseScreen = ({ navigation }: any) => { setAllActivities((prev) => prev.filter((activity) => activity.id !== workoutId) ); + + // eventBus로 운동 삭제 이벤트 발생 (캘린더 새로고침용) + eventBus.emit("workoutSessionDeleted", { + sessionId: sessionId || null, + exerciseName: deletedActivity?.title || null, + workoutDate: workoutDate, + }); } catch (e) { console.error("[WORKOUT][DELETE] 삭제 실패:", e); Alert.alert("오류", "운동 삭제 중 오류가 발생했습니다."); diff --git a/src/screens/main/HomeScreen.tsx b/src/screens/main/HomeScreen.tsx index 00d1d97..64b8b96 100644 --- a/src/screens/main/HomeScreen.tsx +++ b/src/screens/main/HomeScreen.tsx @@ -737,7 +737,10 @@ const HomeScreen = ({ navigation }: any) => { try { // 현재 시간 확인 (한국 시간대) const now = new Date(); - const koreaTime = new Date(now.toLocaleString("en-US", { timeZone: "Asia/Seoul" })); + // 한국 시간대로 변환 (UTC+9) + const koreaTimeOffset = 9 * 60; // 한국은 UTC+9 + const utcTime = now.getTime() + (now.getTimezoneOffset() * 60 * 1000); + const koreaTime = new Date(utcTime + (koreaTimeOffset * 60 * 1000)); const hours = koreaTime.getHours(); const minutes = koreaTime.getMinutes(); const currentTime = hours * 60 + minutes; // 분 단위로 변환 @@ -748,25 +751,29 @@ const HomeScreen = ({ navigation }: any) => { let targetMealType: "BREAKFAST" | "LUNCH" | "DINNER" | null = null; let mealTypeName = ""; - // 00:00 - 4:00: 아침 - if (currentTime >= 0 && currentTime < 4 * 60) { + // 00:00 - 10:00: 아침 + if (currentTime >= 0 && currentTime < 10 * 60) { targetMealType = "BREAKFAST"; mealTypeName = "아침"; + console.log('[HOME] 시간대 판단: 아침 (0 <=', currentTime, '< 600)'); } - // 4:00 - 18:00: 점심 - else if (currentTime >= 4 * 60 && currentTime < 18 * 60) { + // 10:00 - 18:00: 점심 + else if (currentTime >= 10 * 60 && currentTime < 18 * 60) { targetMealType = "LUNCH"; mealTypeName = "점심"; + console.log('[HOME] 시간대 판단: 점심 (600 <=', currentTime, '< 1080)'); } // 18:00 - 24:00: 저녁 else if (currentTime >= 18 * 60 && currentTime < 24 * 60) { targetMealType = "DINNER"; mealTypeName = "저녁"; + console.log('[HOME] 시간대 판단: 저녁 (1080 <=', currentTime, '< 1440)'); } - // 00:00 - 4:00 범위가 아니면 아침 (새벽 시간대) + // 예외 처리 (현재는 발생하지 않지만 안전을 위해) else { targetMealType = "BREAKFAST"; mealTypeName = "아침"; + console.log('[HOME] 시간대 판단: 예외 처리 - 아침으로 설정 (currentTime:', currentTime, ')'); } const today = new Date(); @@ -1008,6 +1015,21 @@ const HomeScreen = ({ navigation }: any) => { }; }, []); + // 식사 삭제 이벤트 리스너 + useEffect(() => { + const unsubscribe = eventBus.on("mealDeleted", () => { + console.log("[HOME] 식사 삭제 이벤트 수신, 식단 데이터 새로고침"); + // 식단 추천 데이터 새로고침 + loadMealRecommendation(); + // 주간 진행률 데이터 새로고침 (캘린더 칼로리 업데이트) + loadWeeklyProgress(); + }); + + return () => { + unsubscribe?.(); + }; + }, []); + // 위젯 편집 화면에서 돌아올 때 위젯 순서 다시 불러오기 useEffect(() => { const unsubscribe = navigation.addListener("focus", () => { diff --git a/src/utils/eventBus.ts b/src/utils/eventBus.ts index d5ed160..3f0285b 100644 --- a/src/utils/eventBus.ts +++ b/src/utils/eventBus.ts @@ -4,9 +4,18 @@ type EventMap = { exerciseName?: string | null; workoutDate?: string | null; }; + workoutSessionSaved: { + sessionId?: string | number | null; + exerciseName?: string | null; + workoutDate?: string | null; + }; inbodyUpdated: { measurementDate?: string | null; }; + mealDeleted: { + date?: string | null; + mealId?: number | null; + }; }; type EventKey = keyof EventMap;