From cbace0d4cc984554bee7cd036675179b8a1cd844 Mon Sep 17 00:00:00 2001 From: a06246 Date: Mon, 15 Dec 2025 18:11:49 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=EA=B8=B0=ED=83=80=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/screens/analysis/AnalysisScreen.tsx | 16 +- src/screens/chatbot/ChatbotScreen.tsx | 11 +- src/screens/exercise/ExerciseScreen.tsx | 28 +- src/screens/main/MyPageScreen.tsx | 1450 ++++++++++++----------- src/services/homeAPI.ts | 72 +- 5 files changed, 794 insertions(+), 783 deletions(-) diff --git a/src/screens/analysis/AnalysisScreen.tsx b/src/screens/analysis/AnalysisScreen.tsx index 76e73bb..3131984 100644 --- a/src/screens/analysis/AnalysisScreen.tsx +++ b/src/screens/analysis/AnalysisScreen.tsx @@ -2917,21 +2917,22 @@ const AnalysisScreen = ({ navigation }: any) => { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: "#1c1c1c", + backgroundColor: colors.background, }, header: { - height: 50, justifyContent: "center", alignItems: "center", paddingHorizontal: 20, - paddingTop: 16, - paddingBottom: 16, + paddingVertical: 16, + borderBottomWidth: 1, + borderBottomColor: colors.border, + backgroundColor: colors.background, position: "relative", }, headerTitle: { - fontSize: 18, - fontWeight: "600", - color: "#ffffff", + fontSize: 20, + fontWeight: "700", + color: colors.text, }, content: { flex: 1, @@ -2940,6 +2941,7 @@ const styles = StyleSheet.create({ maxWidth: 400, alignSelf: "center", width: "100%", + paddingTop: 12, paddingHorizontal: 20, paddingBottom: 100, }, diff --git a/src/screens/chatbot/ChatbotScreen.tsx b/src/screens/chatbot/ChatbotScreen.tsx index 86b0fd8..741f821 100644 --- a/src/screens/chatbot/ChatbotScreen.tsx +++ b/src/screens/chatbot/ChatbotScreen.tsx @@ -561,7 +561,10 @@ const ChatbotScreen = ({ navigation }: any) => { 0 ? Math.max(insets.bottom, 4) : 4 }, + { + // 하단바와 적당한 거리 유지 + paddingBottom: 5, + }, ]} > { try { const response = await getTodayWorkoutTime(finalUserId); - setTodayTotalWorkoutSeconds(response.totalSeconds || 0); + console.log("[EXERCISE][TIME] getTodayWorkoutTime 응답 받음:", { + totalSeconds: response.totalSeconds, + response, + }); + const totalSeconds = response.totalSeconds || 0; + setTodayTotalWorkoutSeconds(totalSeconds); + + // 달력의 오늘 운동 시간도 즉시 반영 + const todayStr = formatDateToString(new Date()); + setDailyWorkoutSeconds((prev) => ({ + ...prev, + [todayStr]: totalSeconds, + })); } catch (e) { console.error("[EXERCISE][TIME] 오늘 운동 시간 조회 실패:", e); setTodayTotalWorkoutSeconds(0); @@ -3725,6 +3737,13 @@ const ExerciseScreen = ({ navigation }: any) => { prev.filter((activity) => activity.id !== workoutId) ); + // 진행률 및 달력 데이터 새로고침 + await loadTodayProgress(); + await loadWeeklyCalories(); + if (workoutDate) { + await loadCalendarCalories([workoutDate]); + } + // eventBus로 운동 삭제 이벤트 발생 (캘린더 새로고침용) eventBus.emit("workoutSessionDeleted", { sessionId: sessionId || null, @@ -3817,6 +3836,11 @@ const ExerciseScreen = ({ navigation }: any) => { // 오늘 날짜라면 홈/분석 쪽에서도 시간이 바로 0으로 반영되도록 try { await loadTodayWorkoutTime(); + await loadTodayProgress(); + await loadWeeklyCalories(); + if (targetDateStr) { + await loadCalendarCalories([targetDateStr]); + } } catch (e) { console.error( "[WORKOUT][DELETE_ALL] 오늘 운동 시간 재조회 실패:", @@ -4509,6 +4533,8 @@ const ExerciseScreen = ({ navigation }: any) => { try { await loadTodayWorkoutTime(); // ✅ 오늘 운동 시간 업데이트 await loadWeeklyCalories(); + const todayStr = formatDateToString(new Date()); + await loadCalendarCalories([todayStr]); // ✅ 오늘 캘린더 운동시간 갱신 } catch (error) { console.error( "[PROGRESS] 운동 완료 후 주간 진행률 새로고침 실패:", diff --git a/src/screens/main/MyPageScreen.tsx b/src/screens/main/MyPageScreen.tsx index 73f3760..ad97652 100644 --- a/src/screens/main/MyPageScreen.tsx +++ b/src/screens/main/MyPageScreen.tsx @@ -1,723 +1,727 @@ -// src/screens/main/MyPageScreen.tsx - -import React, { useState, useEffect } from "react"; -import { - View, - Text, - TouchableOpacity, - StyleSheet, - ScrollView, - Alert, - ActivityIndicator, -} from "react-native"; -import { SafeAreaView } from "react-native-safe-area-context"; -import { Ionicons as Icon } from "@expo/vector-icons"; -import AsyncStorage from "@react-native-async-storage/async-storage"; -import { authAPI } from "../../services"; -import AIAnalysisModal from "../../components/modals/AIAnalysisModal"; -import MyPlanModal from "../../components/modals/MyPlanModal"; -import PaymentMethodModal from "../../components/modals/PaymentMethodModal"; -import ProfileEditModal from "../../components/modals/ProfileEditModal"; -import DeleteAccountModal from "../../components/modals/DeleteAccountModal"; -import PremiumModal from "../../components/modals/PremiumModal"; -import { useRoute } from "@react-navigation/native"; -import { useFocusEffect } from "@react-navigation/native"; -import { getProfile } from "@react-native-seoul/kakao-login"; - -const MyPageScreen = ({ navigation }: any) => { - const route = useRoute(); - - const [profileData, setProfileData] = useState(null); - const [loading, setLoading] = useState(true); - const [currentMembershipType, setCurrentMembershipType] = useState< - "FREE" | "PREMIUM" - >("FREE"); - - // 모달 상태 - const [isAIAnalysisModalOpen, setIsAIAnalysisModalOpen] = useState(false); - const [isMyPlanModalOpen, setIsMyPlanModalOpen] = useState(false); - const [isPaymentMethodModalOpen, setIsPaymentMethodModalOpen] = - useState(false); - const [isProfileEditModalOpen, setIsProfileEditModalOpen] = useState(false); - const [isRoutineRecommendModalOpen, setIsRoutineRecommendModalOpen] = - useState(false); - const [isMealRecommendModalOpen, setIsMealRecommendModalOpen] = - useState(false); - const [isDeleteAccountModalOpen, setIsDeleteAccountModalOpen] = - useState(false); - const [isPremiumModalOpen, setIsPremiumModalOpen] = useState(false); - - // 1. 외부에서 넘어온 파라미터 감지 (내 플랜 모달 열기) - useEffect(() => { - if (route.params?.openPremiumModal) { - console.log("🔓 마이페이지 진입: 프리미엄 모달 자동 오픈"); - setIsPremiumModalOpen(true); - navigation.setParams({ openPremiumModal: undefined }); - } - }, [route.params]); - - // 2. 초기 데이터 로드 - useFocusEffect( - React.useCallback(() => { - console.log("🔄 마이페이지 포커스 - 데이터 새로고침"); - fetchProfile(); - loadMembershipType(); - }, []) - ); - - const fetchProfile = async () => { - try { - setLoading(true); - const data = await authAPI.getProfile(); - setProfileData(data); - } catch (error: any) { - console.error("프로필 로드 실패:", error); - if (error.status === 401) navigation.replace("Login"); - } finally { - setLoading(false); - } - }; - - // 멤버십 타입 로드 - const loadMembershipType = async () => { - try { - const membershipType = await AsyncStorage.getItem("membershipType"); - if (membershipType) { - setCurrentMembershipType(membershipType as "FREE" | "PREMIUM"); - } - } catch (error) { - console.error("멤버십 타입 로드 실패:", error); - } - }; - - // 테스트용 멤버십 전환 함수 - const handleToggleMembership = async () => { - const newType = currentMembershipType === "FREE" ? "PREMIUM" : "FREE"; - - Alert.alert( - "멤버십 전환", - `${ - newType === "PREMIUM" ? "프리미엄" : "무료" - } 플랜으로 전환하시겠습니까?`, - [ - { text: "취소", style: "cancel" }, - { - text: "전환", - onPress: async () => { - try { - console.log( - "🔄 멤버십 전환 시작:", - currentMembershipType, - "→", - newType - ); - - // 1. 멤버십 전환 - const result = await authAPI.toggleMembership(); - - // ✅ 2. 프리미엄으로 전환한 경우 토큰 초기화 (무제한 활성화) - if (result.newType === "PREMIUM") { - try { - await authAPI.resetTokens(); - console.log("✅ 프리미엄 전환 + 토큰 초기화 완료"); - } catch (tokenError) { - console.warn("⚠️ 토큰 초기화 실패 (무시):", tokenError); - } - } - - setCurrentMembershipType(result.newType); - - // AsyncStorage에 멤버십 타입 업데이트 (다른 화면에서도 반영되도록) - await AsyncStorage.setItem("membershipType", result.newType); - console.log( - "✅ AsyncStorage에 멤버십 타입 업데이트:", - result.newType - ); - - Alert.alert( - "전환 완료 ✅", - result.message + - (result.newType === "PREMIUM" ? "\n\n 전환 완료" : ""), - [ - { - text: "확인", - onPress: () => { - loadMembershipType(); - }, - }, - ] - ); - - console.log("✅ 멤버십 전환 완료:", result); - } catch (error: any) { - console.error("❌ 멤버십 전환 실패:", error); - Alert.alert( - "오류", - error.message || "멤버십 전환에 실패했습니다." - ); - } - }, - }, - ] - ); - }; - - // ✅ 로그아웃 실행 함수 (즉시 실행용) - const executeLogout = async () => { - try { - await authAPI.logout(); - navigation.replace("Login"); - } catch (e) { - console.error(e); - navigation.replace("Login"); // 에러나도 일단 화면 이동 - } - }; - - // 버튼 클릭 시 확인창 띄우는 로그아웃 핸들러 - const handleLogoutPress = () => { - Alert.alert("로그아웃", "정말로 로그아웃하시겠습니까?", [ - { text: "취소", style: "cancel" }, - { text: "확인", onPress: executeLogout }, - ]); - }; - - const handleDeleteAccount = async () => { - // 프로필 데이터에서 카카오 사용자 여부 확인 - const isKakaoUser = - profileData && - ((profileData as any).loginType === "KAKAO" || - (profileData as any).provider === "KAKAO" || - (profileData as any).kakaoId !== undefined); - - if (isKakaoUser) { - // 카카오 사용자: unlink API 호출 - try { - await authAPI.kakaoUnlink(); - - // 성공하면 카카오 사용자 → 탈퇴 완료 - Alert.alert( - "탈퇴 완료", - "카카오 연결이 해제되었습니다. 그동안 이용해주셔서 감사합니다.", - [ - { - text: "확인", - onPress: async () => { - // 로컬 데이터 삭제 - await AsyncStorage.removeItem("access_token"); - await AsyncStorage.removeItem("refresh_token"); - await AsyncStorage.removeItem("userId"); - await AsyncStorage.removeItem("userName"); - await AsyncStorage.removeItem("membershipType"); - await AsyncStorage.removeItem("chatbot_tokens"); - navigation.replace("Login"); - }, - }, - ] - ); - } catch (error: any) { - console.error("카카오 unlink 실패:", error); - Alert.alert( - "오류", - error.message || "카카오 연결 해제에 실패했습니다." - ); - } - } else { - // 일반 사용자: 회원탈퇴 모달 열기 - setIsDeleteAccountModalOpen(true); - } - }; - - const getMembershipTypeText = (type: string) => { - switch (type) { - case "FREE": - return "무료 회원"; - case "PREMIUM": - return "프리미엄 회원"; - case "VIP": - return "풀업의 신"; - default: - return "무료 회원"; - } - }; - - if (loading) { - return ( - - - - 프로필을 불러오는 중... - - - ); - } - - if (!profileData) { - return ( - - - 프로필 정보를 불러올 수 없습니다 - - 다시 시도 - - - - ); - } - - return ( - - - 마이페이지 - - - - {/* 프로필 카드 */} - - - - - - - - - {profileData?.name || "사용자"}님 - - - - - {getMembershipTypeText(currentMembershipType)} - - {/* 현재 멤버십 상태 표시 */} - - - - - - - - setIsProfileEditModalOpen(true)} - > - - - - - - - {/* 테스트 섹션 */} - - 🧪 개발 테스트 (개발 전용) - - {/* 멤버십 전환 버튼 */} - - - - {currentMembershipType === "FREE" ? "🆓 → 💎" : "💎 → 🆓"}{" "} - 무료/유료 전환 - - - - {currentMembershipType === "FREE" ? "FREE" : "PREMIUM"} - - - - - - - - - - - - - {/* 구독/결제 */} - - 💳 구독/결제 - - setIsPremiumModalOpen(true)} - > - 구독 하기 - - - - - - setIsPaymentMethodModalOpen(true)} - > - 내 플랜 보기 - - - - - - - - {/* 추천 내역 */} - - 🌟 추천 내역 - - - - navigation.navigate("RoutineRecommend")} - > - 운동 추천 내역 - - - - - - navigation.navigate("MealRecommendHistory")} - > - 식단 추천 내역 - - - - - - - - {/* 계정 관리 */} - - ⚙️ 계정 관리 - - {/* ✅ 버튼에 확인창 있는 로그아웃 연결 */} - - 로그아웃 - - - - - - - 회원탈퇴 - - - - - - - {/* 모달들 */} - - {/* ✅ 1. 구독 정보 모달 (로그아웃 기능 연결됨) */} - setIsPaymentMethodModalOpen(false)} - onCancelSuccess={executeLogout} - /> - - setIsAIAnalysisModalOpen(false)} - /> - - setIsMyPlanModalOpen(false)} - navigation={navigation} - /> - - setIsProfileEditModalOpen(false)} - profileData={profileData} - onProfileUpdate={fetchProfile} - /> - - setIsDeleteAccountModalOpen(false)} - onDeleteSuccess={() => navigation.replace("Login")} - /> - setIsPremiumModalOpen(false)} - /> - - ); -}; - -const NEW_COLORS = { - background: "#1a1a1a", - text: "#f0f0f0", - text_secondary: "#a0a0a0", - accent: "#e3ff7c", - card_bg: "#252525", - separator: "#3a3a3a", - delete_color: "#ff6b6b", -}; - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: NEW_COLORS.background, - }, - centerContent: { - justifyContent: "center", - alignItems: "center", - }, - loadingText: { - marginTop: 16, - fontSize: 16, - color: NEW_COLORS.text, - }, - errorText: { - fontSize: 16, - color: NEW_COLORS.delete_color, - marginBottom: 16, - }, - retryBtn: { - paddingHorizontal: 24, - paddingVertical: 12, - backgroundColor: NEW_COLORS.separator, - borderRadius: 8, - }, - retryBtnText: { - color: NEW_COLORS.text, - fontSize: 16, - fontWeight: "600", - }, - header: { - paddingVertical: 16, - paddingHorizontal: 20, - alignItems: "center", - justifyContent: "center", - }, - headerTitle: { - fontSize: 20, - fontWeight: "700", - color: NEW_COLORS.text, - }, - content: { - flex: 1, - paddingHorizontal: 20, - }, - profileCard: { - marginTop: 16, - marginBottom: 30, - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - padding: 20, - backgroundColor: NEW_COLORS.card_bg, - borderRadius: 15, - }, - profileInfo: { - flexDirection: "row", - alignItems: "center", - gap: 16, - flex: 1, - }, - profileAvatar: { - width: 60, - height: 60, - borderRadius: 30, - backgroundColor: NEW_COLORS.separator, - justifyContent: "center", - alignItems: "center", - }, - profileDetails: { - flex: 1, - }, - username: { - flexDirection: "row", - alignItems: "center", - gap: 6, - marginBottom: 4, - }, - usernameText: { - fontSize: 20, - fontWeight: "bold", - color: NEW_COLORS.text, - }, - membershipBadgeContainer: { - flexDirection: "row", - alignItems: "center", - gap: 8, - }, - userTitle: { - fontSize: 14, - color: NEW_COLORS.accent, - fontWeight: "500", - }, - membershipStatusBadge: { - paddingHorizontal: 6, - paddingVertical: 2, - borderRadius: 8, - backgroundColor: NEW_COLORS.separator, - }, - premiumStatusBadge: { - backgroundColor: "#FFD70020", - }, - editProfileButton: { - padding: 8, - backgroundColor: NEW_COLORS.separator, - borderRadius: 10, - }, - section: { - paddingVertical: 16, - marginBottom: 8, - }, - separator: { - height: 1, - backgroundColor: NEW_COLORS.separator, - marginVertical: 10, - }, - subSeparator: { - height: 1, - backgroundColor: NEW_COLORS.separator, - marginVertical: 4, - marginLeft: 10, - }, - sectionTitle: { - fontSize: 18, - fontWeight: "600", - color: NEW_COLORS.text, - }, - sectionLinks: { - backgroundColor: NEW_COLORS.card_bg, - borderRadius: 15, - paddingHorizontal: 15, - paddingVertical: 5, - marginTop: 8, - }, - linkItem: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - paddingVertical: 14, - }, - linkText: { - fontSize: 16, - color: NEW_COLORS.text, - fontWeight: "400", - }, - logoutText: { - color: NEW_COLORS.accent, - fontWeight: "500", - }, - deleteText: { - color: NEW_COLORS.delete_color, - fontWeight: "500", - }, - linkItemWithBadge: { - flexDirection: "row", - alignItems: "center", - gap: 8, - }, - newBadge: { - backgroundColor: NEW_COLORS.accent, - paddingHorizontal: 8, - paddingVertical: 2, - borderRadius: 8, - }, - newBadgeText: { - fontSize: 10, - fontWeight: "700", - color: "#000", - }, - statusBadge: { - backgroundColor: NEW_COLORS.separator, - paddingHorizontal: 10, - paddingVertical: 3, - borderRadius: 10, - }, - premiumBadge: { - backgroundColor: "#FFD70020", - }, - statusBadgeText: { - fontSize: 11, - fontWeight: "700", - color: NEW_COLORS.accent, - }, -}); - -export default MyPageScreen; +// src/screens/main/MyPageScreen.tsx + +import React, { useState, useEffect } from "react"; +import { + View, + Text, + TouchableOpacity, + StyleSheet, + ScrollView, + Alert, + ActivityIndicator, +} from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { Ionicons as Icon } from "@expo/vector-icons"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { authAPI } from "../../services"; +import { colors } from "../../theme/colors"; +import AIAnalysisModal from "../../components/modals/AIAnalysisModal"; +import MyPlanModal from "../../components/modals/MyPlanModal"; +import PaymentMethodModal from "../../components/modals/PaymentMethodModal"; +import ProfileEditModal from "../../components/modals/ProfileEditModal"; +import DeleteAccountModal from "../../components/modals/DeleteAccountModal"; +import PremiumModal from "../../components/modals/PremiumModal"; +import { useRoute } from "@react-navigation/native"; +import { useFocusEffect } from "@react-navigation/native"; +import { getProfile } from "@react-native-seoul/kakao-login"; + +const MyPageScreen = ({ navigation }: any) => { + const route = useRoute(); + + const [profileData, setProfileData] = useState(null); + const [loading, setLoading] = useState(true); + const [currentMembershipType, setCurrentMembershipType] = useState< + "FREE" | "PREMIUM" + >("FREE"); + + // 모달 상태 + const [isAIAnalysisModalOpen, setIsAIAnalysisModalOpen] = useState(false); + const [isMyPlanModalOpen, setIsMyPlanModalOpen] = useState(false); + const [isPaymentMethodModalOpen, setIsPaymentMethodModalOpen] = + useState(false); + const [isProfileEditModalOpen, setIsProfileEditModalOpen] = useState(false); + const [isRoutineRecommendModalOpen, setIsRoutineRecommendModalOpen] = + useState(false); + const [isMealRecommendModalOpen, setIsMealRecommendModalOpen] = + useState(false); + const [isDeleteAccountModalOpen, setIsDeleteAccountModalOpen] = + useState(false); + const [isPremiumModalOpen, setIsPremiumModalOpen] = useState(false); + + // 1. 외부에서 넘어온 파라미터 감지 (내 플랜 모달 열기) + useEffect(() => { + if (route.params?.openPremiumModal) { + console.log("🔓 마이페이지 진입: 프리미엄 모달 자동 오픈"); + setIsPremiumModalOpen(true); + navigation.setParams({ openPremiumModal: undefined }); + } + }, [route.params]); + + // 2. 초기 데이터 로드 + useFocusEffect( + React.useCallback(() => { + console.log("🔄 마이페이지 포커스 - 데이터 새로고침"); + fetchProfile(); + loadMembershipType(); + }, []) + ); + + const fetchProfile = async () => { + try { + setLoading(true); + const data = await authAPI.getProfile(); + setProfileData(data); + } catch (error: any) { + console.error("프로필 로드 실패:", error); + if (error.status === 401) navigation.replace("Login"); + } finally { + setLoading(false); + } + }; + + // 멤버십 타입 로드 + const loadMembershipType = async () => { + try { + const membershipType = await AsyncStorage.getItem("membershipType"); + if (membershipType) { + setCurrentMembershipType(membershipType as "FREE" | "PREMIUM"); + } + } catch (error) { + console.error("멤버십 타입 로드 실패:", error); + } + }; + + // 테스트용 멤버십 전환 함수 + const handleToggleMembership = async () => { + const newType = currentMembershipType === "FREE" ? "PREMIUM" : "FREE"; + + Alert.alert( + "멤버십 전환", + `${ + newType === "PREMIUM" ? "프리미엄" : "무료" + } 플랜으로 전환하시겠습니까?`, + [ + { text: "취소", style: "cancel" }, + { + text: "전환", + onPress: async () => { + try { + console.log( + "🔄 멤버십 전환 시작:", + currentMembershipType, + "→", + newType + ); + + // 1. 멤버십 전환 + const result = await authAPI.toggleMembership(); + + // ✅ 2. 프리미엄으로 전환한 경우 토큰 초기화 (무제한 활성화) + if (result.newType === "PREMIUM") { + try { + await authAPI.resetTokens(); + console.log("✅ 프리미엄 전환 + 토큰 초기화 완료"); + } catch (tokenError) { + console.warn("⚠️ 토큰 초기화 실패 (무시):", tokenError); + } + } + + setCurrentMembershipType(result.newType); + + // AsyncStorage에 멤버십 타입 업데이트 (다른 화면에서도 반영되도록) + await AsyncStorage.setItem("membershipType", result.newType); + console.log( + "✅ AsyncStorage에 멤버십 타입 업데이트:", + result.newType + ); + + Alert.alert( + "전환 완료 ✅", + result.message + + (result.newType === "PREMIUM" ? "\n\n 전환 완료" : ""), + [ + { + text: "확인", + onPress: () => { + loadMembershipType(); + }, + }, + ] + ); + + console.log("✅ 멤버십 전환 완료:", result); + } catch (error: any) { + console.error("❌ 멤버십 전환 실패:", error); + Alert.alert( + "오류", + error.message || "멤버십 전환에 실패했습니다." + ); + } + }, + }, + ] + ); + }; + + // ✅ 로그아웃 실행 함수 (즉시 실행용) + const executeLogout = async () => { + try { + await authAPI.logout(); + navigation.replace("Login"); + } catch (e) { + console.error(e); + navigation.replace("Login"); // 에러나도 일단 화면 이동 + } + }; + + // 버튼 클릭 시 확인창 띄우는 로그아웃 핸들러 + const handleLogoutPress = () => { + Alert.alert("로그아웃", "정말로 로그아웃하시겠습니까?", [ + { text: "취소", style: "cancel" }, + { text: "확인", onPress: executeLogout }, + ]); + }; + + const handleDeleteAccount = async () => { + // 프로필 데이터에서 카카오 사용자 여부 확인 + const isKakaoUser = + profileData && + ((profileData as any).loginType === "KAKAO" || + (profileData as any).provider === "KAKAO" || + (profileData as any).kakaoId !== undefined); + + if (isKakaoUser) { + // 카카오 사용자: unlink API 호출 + try { + await authAPI.kakaoUnlink(); + + // 성공하면 카카오 사용자 → 탈퇴 완료 + Alert.alert( + "탈퇴 완료", + "카카오 연결이 해제되었습니다. 그동안 이용해주셔서 감사합니다.", + [ + { + text: "확인", + onPress: async () => { + // 로컬 데이터 삭제 + await AsyncStorage.removeItem("access_token"); + await AsyncStorage.removeItem("refresh_token"); + await AsyncStorage.removeItem("userId"); + await AsyncStorage.removeItem("userName"); + await AsyncStorage.removeItem("membershipType"); + await AsyncStorage.removeItem("chatbot_tokens"); + navigation.replace("Login"); + }, + }, + ] + ); + } catch (error: any) { + console.error("카카오 unlink 실패:", error); + Alert.alert( + "오류", + error.message || "카카오 연결 해제에 실패했습니다." + ); + } + } else { + // 일반 사용자: 회원탈퇴 모달 열기 + setIsDeleteAccountModalOpen(true); + } + }; + + const getMembershipTypeText = (type: string) => { + switch (type) { + case "FREE": + return "무료 회원"; + case "PREMIUM": + return "프리미엄 회원"; + case "VIP": + return "풀업의 신"; + default: + return "무료 회원"; + } + }; + + if (loading) { + return ( + + + + 프로필을 불러오는 중... + + + ); + } + + if (!profileData) { + return ( + + + 프로필 정보를 불러올 수 없습니다 + + 다시 시도 + + + + ); + } + + return ( + + + 마이페이지 + + + + {/* 프로필 카드 */} + + + + + + + + + {profileData?.name || "사용자"}님 + + + + + {getMembershipTypeText(currentMembershipType)} + + {/* 현재 멤버십 상태 표시 */} + + + + + + + + setIsProfileEditModalOpen(true)} + > + + + + + + + {/* 테스트 섹션 */} + + 🧪 개발 테스트 (개발 전용) + + {/* 멤버십 전환 버튼 */} + + + + {currentMembershipType === "FREE" ? "🆓 → 💎" : "💎 → 🆓"}{" "} + 무료/유료 전환 + + + + {currentMembershipType === "FREE" ? "FREE" : "PREMIUM"} + + + + + + + + + + + + + {/* 구독/결제 */} + + 💳 구독/결제 + + setIsPremiumModalOpen(true)} + > + 구독 하기 + + + + + + setIsPaymentMethodModalOpen(true)} + > + 내 플랜 보기 + + + + + + + + {/* 추천 내역 */} + + 🌟 추천 내역 + + + + navigation.navigate("RoutineRecommend")} + > + 운동 추천 내역 + + + + + + navigation.navigate("MealRecommendHistory")} + > + 식단 추천 내역 + + + + + + + + {/* 계정 관리 */} + + ⚙️ 계정 관리 + + {/* ✅ 버튼에 확인창 있는 로그아웃 연결 */} + + 로그아웃 + + + + + + + 회원탈퇴 + + + + + + + {/* 모달들 */} + + {/* ✅ 1. 구독 정보 모달 (로그아웃 기능 연결됨) */} + setIsPaymentMethodModalOpen(false)} + onCancelSuccess={executeLogout} + /> + + setIsAIAnalysisModalOpen(false)} + /> + + setIsMyPlanModalOpen(false)} + navigation={navigation} + /> + + setIsProfileEditModalOpen(false)} + profileData={profileData} + onProfileUpdate={fetchProfile} + /> + + setIsDeleteAccountModalOpen(false)} + onDeleteSuccess={() => navigation.replace("Login")} + /> + setIsPremiumModalOpen(false)} + /> + + ); +}; + +const NEW_COLORS = { + background: "#1a1a1a", + text: "#f0f0f0", + text_secondary: "#a0a0a0", + accent: "#e3ff7c", + card_bg: "#252525", + separator: "#3a3a3a", + delete_color: "#ff6b6b", +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: NEW_COLORS.background, + }, + centerContent: { + justifyContent: "center", + alignItems: "center", + }, + loadingText: { + marginTop: 16, + fontSize: 16, + color: NEW_COLORS.text, + }, + errorText: { + fontSize: 16, + color: NEW_COLORS.delete_color, + marginBottom: 16, + }, + retryBtn: { + paddingHorizontal: 24, + paddingVertical: 12, + backgroundColor: NEW_COLORS.separator, + borderRadius: 8, + }, + retryBtnText: { + color: NEW_COLORS.text, + fontSize: 16, + fontWeight: "600", + }, + header: { + paddingVertical: 16, + paddingHorizontal: 20, + backgroundColor: colors.background, + borderBottomWidth: 1, + borderBottomColor: colors.border, + alignItems: "center", + justifyContent: "center", + }, + headerTitle: { + fontSize: 20, + fontWeight: "700", + color: colors.text, + }, + content: { + flex: 1, + paddingHorizontal: 20, + }, + profileCard: { + marginTop: 16, + marginBottom: 30, + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + padding: 20, + backgroundColor: NEW_COLORS.card_bg, + borderRadius: 15, + }, + profileInfo: { + flexDirection: "row", + alignItems: "center", + gap: 16, + flex: 1, + }, + profileAvatar: { + width: 60, + height: 60, + borderRadius: 30, + backgroundColor: NEW_COLORS.separator, + justifyContent: "center", + alignItems: "center", + }, + profileDetails: { + flex: 1, + }, + username: { + flexDirection: "row", + alignItems: "center", + gap: 6, + marginBottom: 4, + }, + usernameText: { + fontSize: 20, + fontWeight: "bold", + color: NEW_COLORS.text, + }, + membershipBadgeContainer: { + flexDirection: "row", + alignItems: "center", + gap: 8, + }, + userTitle: { + fontSize: 14, + color: NEW_COLORS.accent, + fontWeight: "500", + }, + membershipStatusBadge: { + paddingHorizontal: 6, + paddingVertical: 2, + borderRadius: 8, + backgroundColor: NEW_COLORS.separator, + }, + premiumStatusBadge: { + backgroundColor: "#FFD70020", + }, + editProfileButton: { + padding: 8, + backgroundColor: NEW_COLORS.separator, + borderRadius: 10, + }, + section: { + paddingVertical: 16, + marginBottom: 8, + }, + separator: { + height: 1, + backgroundColor: NEW_COLORS.separator, + marginVertical: 10, + }, + subSeparator: { + height: 1, + backgroundColor: NEW_COLORS.separator, + marginVertical: 4, + marginLeft: 10, + }, + sectionTitle: { + fontSize: 18, + fontWeight: "600", + color: NEW_COLORS.text, + }, + sectionLinks: { + backgroundColor: NEW_COLORS.card_bg, + borderRadius: 15, + paddingHorizontal: 15, + paddingVertical: 5, + marginTop: 8, + }, + linkItem: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingVertical: 14, + }, + linkText: { + fontSize: 16, + color: NEW_COLORS.text, + fontWeight: "400", + }, + logoutText: { + color: NEW_COLORS.accent, + fontWeight: "500", + }, + deleteText: { + color: NEW_COLORS.delete_color, + fontWeight: "500", + }, + linkItemWithBadge: { + flexDirection: "row", + alignItems: "center", + gap: 8, + }, + newBadge: { + backgroundColor: NEW_COLORS.accent, + paddingHorizontal: 8, + paddingVertical: 2, + borderRadius: 8, + }, + newBadgeText: { + fontSize: 10, + fontWeight: "700", + color: "#000", + }, + statusBadge: { + backgroundColor: NEW_COLORS.separator, + paddingHorizontal: 10, + paddingVertical: 3, + borderRadius: 10, + }, + premiumBadge: { + backgroundColor: "#FFD70020", + }, + statusBadgeText: { + fontSize: 11, + fontWeight: "700", + color: NEW_COLORS.accent, + }, +}); + +export default MyPageScreen; diff --git a/src/services/homeAPI.ts b/src/services/homeAPI.ts index 6ad9d91..a0544ad 100644 --- a/src/services/homeAPI.ts +++ b/src/services/homeAPI.ts @@ -60,6 +60,24 @@ const getUserId = async (): Promise => { } }; +// 코멘트 응답(JSON 문자열 또는 텍스트)에서 랜덤 한 줄을 추출 +const extractRandomComment = (raw: string): string => { + if (!raw) return ""; + try { + const parsed = JSON.parse(raw); + if (Array.isArray(parsed?.comments) && parsed.comments.length > 0) { + const idx = Math.floor(Math.random() * parsed.comments.length); + return parsed.comments[idx] ?? ""; + } + if (typeof parsed?.comment === "string") { + return parsed.comment; + } + } catch { + // 파싱 실패 시 raw 그대로 사용 + } + return raw; +}; + export const homeAPI = { // 홈 화면 메인 정보 조회 getHomeData: async (date?: string): Promise => { @@ -217,14 +235,7 @@ export const homeAPI = { const data = await response.text(); // 문자열 반환 console.log('[HOME] 일일 운동 코멘트 응답:', data); - - // JSON 문자열인 경우 파싱하여 comment 필드만 추출 - try { - const parsed = JSON.parse(data); - return parsed.comment || data || ''; - } catch { - return data || ''; - } + return extractRandomComment(data) || ''; } catch (error: any) { console.error('[HOME] 일일 운동 코멘트 API 호출 실패:', error); return ''; @@ -252,14 +263,7 @@ export const homeAPI = { const data = await response.text(); console.log('[HOME] 주간 운동 코멘트 응답:', data); - - // JSON 문자열인 경우 파싱하여 comment 필드만 추출 - try { - const parsed = JSON.parse(data); - return parsed.comment || data || ''; - } catch { - return data || ''; - } + return extractRandomComment(data) || ''; } catch (error: any) { console.error('[HOME] 주간 운동 코멘트 API 호출 실패:', error); return ''; @@ -287,14 +291,7 @@ export const homeAPI = { const data = await response.text(); console.log('[HOME] 월간 운동 코멘트 응답:', data); - - // JSON 문자열인 경우 파싱하여 comment 필드만 추출 - try { - const parsed = JSON.parse(data); - return parsed.comment || data || ''; - } catch { - return data || ''; - } + return extractRandomComment(data) || ''; } catch (error: any) { console.error('[HOME] 월간 운동 코멘트 API 호출 실패:', error); return ''; @@ -322,14 +319,7 @@ export const homeAPI = { const data = await response.text(); console.log('[HOME] 일일 영양 코멘트 응답:', data); - - // JSON 문자열인 경우 파싱하여 comment 필드만 추출 - try { - const parsed = JSON.parse(data); - return parsed.comment || data || ''; - } catch { - return data || ''; - } + return extractRandomComment(data) || ''; } catch (error: any) { console.error('[HOME] 일일 영양 코멘트 API 호출 실패:', error); return ''; @@ -357,14 +347,7 @@ export const homeAPI = { const data = await response.text(); console.log('[HOME] 주간 영양 코멘트 응답:', data); - - // JSON 문자열인 경우 파싱하여 comment 필드만 추출 - try { - const parsed = JSON.parse(data); - return parsed.comment || data || ''; - } catch { - return data || ''; - } + return extractRandomComment(data) || ''; } catch (error: any) { console.error('[HOME] 주간 영양 코멘트 API 호출 실패:', error); return ''; @@ -392,14 +375,7 @@ export const homeAPI = { const data = await response.text(); console.log('[HOME] 월간 영양 코멘트 응답:', data); - - // JSON 문자열인 경우 파싱하여 comment 필드만 추출 - try { - const parsed = JSON.parse(data); - return parsed.comment || data || ''; - } catch { - return data || ''; - } + return extractRandomComment(data) || ''; } catch (error: any) { console.error('[HOME] 월간 영양 코멘트 API 호출 실패:', error); return ''; From efe1c3fcc55d24d3eb06a2478f11a4940008d5a3 Mon Sep 17 00:00:00 2001 From: a06246 Date: Mon, 15 Dec 2025 18:11:49 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=EA=B8=B0=ED=83=80=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/screens/exercise/ExerciseScreen.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/screens/exercise/ExerciseScreen.tsx b/src/screens/exercise/ExerciseScreen.tsx index 447cd03..2c65769 100644 --- a/src/screens/exercise/ExerciseScreen.tsx +++ b/src/screens/exercise/ExerciseScreen.tsx @@ -59,6 +59,10 @@ interface Activity { externalId?: string; // 운동/스트레칭 외부 ID saveTitle?: string; groupKey?: string; + category?: string; + title?: string; + targetMuscle?: string; + bodyPart?: string; } const DETAIL_TIME_REGEX = /(오전|오후)\s*\d{1,2}:\d{2}/gi;