diff --git a/src/screens/analysis/HealthScoreTrendScreen.tsx b/src/screens/analysis/HealthScoreTrendScreen.tsx index dc7ad99..5fb7f32 100644 --- a/src/screens/analysis/HealthScoreTrendScreen.tsx +++ b/src/screens/analysis/HealthScoreTrendScreen.tsx @@ -1,393 +1,506 @@ -// src/screens/HealthScoreTrendScreen.tsx -import React, { useState, useEffect } from "react"; -import { - View, - Text, - StyleSheet, - ScrollView, - ActivityIndicator, - TouchableOpacity, - Dimensions, -} from "react-native"; -import { SafeAreaView } from "react-native-safe-area-context"; -import { Ionicons as Icon } from "@expo/vector-icons"; -import { LineChart } from "react-native-chart-kit"; -import { healthScoreAPI, ScoreTrendItem } from "../../services"; -import { colors } from "../../theme/colors"; - -type PeriodType = "daily" | "weekly" | "monthly"; - -const HealthScoreTrendScreen = ({ navigation }: any) => { - const [period, setPeriod] = useState("daily"); - const [trendData, setTrendData] = useState([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { - loadTrendData(); - }, [period]); - - const loadTrendData = async () => { - try { - setLoading(true); - let data: ScoreTrendItem[] = []; - - if (period === "daily") { - data = await healthScoreAPI.getDailyTrend(); - } else if (period === "weekly") { - data = await healthScoreAPI.getWeeklyTrend(); - } else { - data = await healthScoreAPI.getMonthlyTrend(); - } - - console.log("🎯 [SCREEN] λ°›μ•„μ˜¨ 데이터:", data); - console.log("🎯 [SCREEN] 데이터 길이:", data.length); - console.log("🎯 [SCREEN] 첫 번째 μ•„μ΄ν…œ:", data[0]); - - // μœ νš¨ν•œ λ°μ΄ν„°λ§Œ 필터링 - const validData = data.filter( - (item) => - item && - typeof item.total === "number" && - !isNaN(item.total) && - item.date - ); - - console.log("βœ… [SCREEN] μœ νš¨ν•œ 데이터:", validData); - console.log("βœ… [SCREEN] μœ νš¨ν•œ 데이터 길이:", validData.length); - - setTrendData(validData); - } catch (error) { - console.error("κ±΄κ°•μ μˆ˜ νŠΈλ Œλ“œ λ‘œλ“œ μ‹€νŒ¨:", error); - setTrendData([]); - } finally { - setLoading(false); - } - }; - - // 차트 데이터 생성 μ‹œ μΆ”κ°€ 검증 - const chartData = { - labels: - trendData.length > 0 - ? trendData.map((item) => { - const date = new Date(item.date); - return period === "monthly" - ? `${date.getMonth() + 1}μ›”` - : `${date.getMonth() + 1}/${date.getDate()}`; - }) - : [""], - datasets: [ - { - data: - trendData.length > 0 - ? trendData.map((item) => { - const value = Number(item.total); - return isNaN(value) ? 0 : value; - }) - : [0], - color: (opacity = 1) => `rgba(227, 255, 124, ${opacity})`, - strokeWidth: 3, - }, - ], - }; - - // 졜근 점수 계산 μ‹œ 검증 - const latestScore = - trendData.length > 0 - ? Math.round(trendData[trendData.length - 1].total || 0) - : 0; - - // 평균 점수 계산 μ‹œ 검증 - const averageScore = - trendData.length > 0 - ? Math.round( - trendData.reduce((sum, item) => { - const value = Number(item.total); - return sum + (isNaN(value) ? 0 : value); - }, 0) / trendData.length - ) - : 0; - - return ( - - {/* 헀더 */} - - navigation.goBack()} - > - - - κ±΄κ°•μ μˆ˜ 좔이 - - - - - {/* κΈ°κ°„ 선택 νƒ­ */} - - setPeriod("daily")} - > - - 일간 - - - setPeriod("weekly")} - > - - μ£Όκ°„ - - - setPeriod("monthly")} - > - - μ›”κ°„ - - - - - {loading ? ( - - - λΆˆλŸ¬μ˜€λŠ” 쀑... - - ) : trendData.length === 0 ? ( - - - - 식단과 μš΄λ™κΈ°λ‘μ΄ μ—†μ–΄ κ±΄κ°•μ μˆ˜λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€ - - - 식단과 μš΄λ™μ„ κΈ°λ‘ν•˜λ©΄ μ μˆ˜κ°€ μƒμ„±λ©λ‹ˆλ‹€ - - - ) : ( - <> - {/* μš”μ•½ μΉ΄λ“œ */} - - - 졜근 점수 - {latestScore} - - - 평균 점수 - {averageScore} - - - - {/* κ·Έλž˜ν”„ */} - - `rgba(227, 255, 124, ${opacity})`, - labelColor: (opacity = 1) => - `rgba(255, 255, 255, ${opacity})`, - style: { - borderRadius: 16, - }, - propsForDots: { - r: "4", - strokeWidth: "2", - stroke: "#E3FF7C", - }, - }} - bezier - style={styles.chart} - /> - - - {/* 점수 νžˆμŠ€ν† λ¦¬ */} - - 점수 기둝 - {trendData - .slice() - .reverse() - .map((item, index) => { - const score = Math.round(item.total || 0); // ⭐ NaN λ°©μ§€ - return ( - - {item.date} - {score}점 - - ); - })} - - - )} - - - ); -}; - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: "#1c1c1c", - }, - header: { - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", - paddingHorizontal: 20, - paddingVertical: 16, - }, - backButton: { - width: 40, - height: 40, - justifyContent: "center", - }, - headerTitle: { - fontSize: 18, - fontWeight: "600", - color: "#ffffff", - }, - headerRight: { - width: 40, - }, - content: { - flex: 1, - paddingHorizontal: 20, - }, - periodTabs: { - flexDirection: "row", - backgroundColor: "#2a2a2a", - borderRadius: 12, - padding: 4, - marginBottom: 20, - }, - periodTab: { - flex: 1, - paddingVertical: 10, - alignItems: "center", - borderRadius: 8, - }, - periodTabActive: { - backgroundColor: "#E3FF7C", - }, - periodTabText: { - fontSize: 14, - fontWeight: "600", - color: "#666", - }, - periodTabTextActive: { - color: "#000", - }, - loadingContainer: { - paddingVertical: 60, - alignItems: "center", - gap: 12, - }, - loadingText: { - fontSize: 14, - color: "#aaa", - }, - emptyContainer: { - paddingVertical: 60, - alignItems: "center", - gap: 12, - }, - emptyText: { - fontSize: 16, - fontWeight: "600", - color: "#ffffff", - }, - emptySubText: { - fontSize: 14, - color: "#aaa", - }, - summaryCards: { - flexDirection: "row", - gap: 12, - marginBottom: 20, - }, - summaryCard: { - flex: 1, - backgroundColor: "#2a2a2a", - borderRadius: 12, - padding: 16, - alignItems: "center", - }, - summaryLabel: { - fontSize: 12, - color: "#aaa", - marginBottom: 8, - }, - summaryValue: { - fontSize: 32, - fontWeight: "700", - color: "#E3FF7C", - }, - chartContainer: { - alignItems: "center", - marginBottom: 20, - }, - chart: { - borderRadius: 16, - }, - historySection: { - backgroundColor: "#2a2a2a", - borderRadius: 12, - padding: 16, - marginBottom: 20, - }, - historyTitle: { - fontSize: 16, - fontWeight: "600", - color: "#ffffff", - marginBottom: 16, - }, - historyItem: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - paddingVertical: 12, - borderBottomWidth: 1, - borderBottomColor: "#333", - }, - historyDate: { - fontSize: 14, - color: "#aaa", - }, - historyScore: { - fontSize: 16, - fontWeight: "600", - color: "#E3FF7C", - }, -}); - -export default HealthScoreTrendScreen; +// src/screens/HealthScoreTrendScreen.tsx +import React, { useState, useEffect } from "react"; +import { + View, + Text, + StyleSheet, + ScrollView, + ActivityIndicator, + TouchableOpacity, + Dimensions, +} from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { Ionicons as Icon } from "@expo/vector-icons"; +import { LineChart } from "react-native-chart-kit"; +import { healthScoreAPI, ScoreTrendItem } from "../../services"; +import { colors } from "../../theme/colors"; + +type PeriodType = "daily" | "weekly" | "monthly"; + +const HealthScoreTrendScreen = ({ navigation }: any) => { + const [period, setPeriod] = useState("daily"); + const [trendData, setTrendData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + loadTrendData(); + }, [period]); + + const loadTrendData = async () => { + try { + setLoading(true); + let data: ScoreTrendItem[] = []; + + if (period === "daily") { + data = await healthScoreAPI.getDailyTrend(); + } else if (period === "weekly") { + data = await healthScoreAPI.getWeeklyTrend(); + } else { + data = await healthScoreAPI.getMonthlyTrend(); + } + + console.log(`🎯 [SCREEN] ${period} 데이터 λ‘œλ“œ:`, data); + + // μœ νš¨ν•œ λ°μ΄ν„°λ§Œ 필터링 + const validData = data.filter( + (item) => + item && + typeof item.total === "number" && + !isNaN(item.total) && + item.date + ); + + // λ‚ μ§œμˆœ μ˜€λ¦„μ°¨μˆœ μ •λ ¬ (κ³Όκ±° -> 미래) + validData.sort( + (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime() + ); + + setTrendData(validData); + } catch (error) { + console.error("κ±΄κ°•μ μˆ˜ νŠΈλ Œλ“œ λ‘œλ“œ μ‹€νŒ¨:", error); + setTrendData([]); + } finally { + setLoading(false); + } + }; + + // βœ… [톡합 μˆ˜μ •] 기간별 κ³ μ • μΆ• 데이터 생성 ν•¨μˆ˜ (였늘 κΈ°μ€€) + // 데이터가 없어도 λ‚ μ§œ 좕을 κ³ μ •ν•΄μ„œ λ³΄μ—¬μ€λ‹ˆλ‹€. + const getProcessedGraphData = () => { + const today = new Date(); + const result = []; + + // πŸ› οΈ 둜컬 μ‹œκ°„ κΈ°μ€€ λ‚ μ§œ λ¬Έμžμ—΄ λ³€ν™˜ 헬퍼 (YYYY-MM-DD) + const getLocalDateString = (date: Date) => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; + }; + + if (period === "daily") { + // 🟒 일간: 였늘 포함 졜근 5일 + for (let i = 4; i >= 0; i--) { + const d = new Date(today); + d.setDate(today.getDate() - i); + + // 🚨 μˆ˜μ •λ¨: ISOString λŒ€μ‹  둜컬 μ‹œκ°„ μ‚¬μš© + const dateKey = getLocalDateString(d); + + // λ‚ μ§œκ°€ μ •ν™•νžˆ μΌμΉ˜ν•˜λŠ”μ§€ 확인 + const foundData = trendData.find((item) => item.date === dateKey); + + result.push({ + date: d, + score: foundData ? Number(foundData.total) : 0, + }); + } + } else if (period === "weekly") { + // 🟒 μ£Όκ°„: 이번 μ£Ό 포함 졜근 5μ£Ό + for (let i = 4; i >= 0; i--) { + const d = new Date(today); + d.setDate(today.getDate() - i * 7); + + // ν•΄λ‹Ή 주차의 μ‹œμž‘μΌκ³Ό μ’…λ£ŒμΌ 계산 (λŒ€λž΅μ  λ²”μœ„) + const weekEnd = new Date(d); + weekEnd.setHours(23, 59, 59, 999); + + const weekStart = new Date(d); + weekStart.setDate(d.getDate() - 6); + weekStart.setHours(0, 0, 0, 0); + + // ν•΄λ‹Ή μ£Όκ°„ λ²”μœ„(Start ~ End)에 ν¬ν•¨λ˜λŠ” 데이터 쀑 κ°€μž₯ μ΅œμ‹  것 μ°ΎκΈ° + // (λ˜λŠ” ν•΄λ‹Ή μ£Όκ°„μ˜ 평균을 λ‚΄κ³  μ‹Άλ‹€λ©΄ 둜직 λ³€κ²½ κ°€λŠ₯) + const foundData = trendData.find((item) => { + const itemDate = new Date(item.date); + // λ‚ μ§œ 비ꡐ μ‹œ μ‹œκ°„ 간섭을 ν”Όν•˜κΈ° μœ„ν•΄ λ‚ μ§œ λ¬Έμžμ—΄λ‘œ 비ꡐ ꢌμž₯λ˜λ‚˜, + // μ—¬κΈ°μ„œλŠ” λ²”μœ„ 체크λ₯Ό μœ„ν•΄ Date 객체 비ꡐ μ‚¬μš© + return itemDate >= weekStart && itemDate <= weekEnd; + }); + + result.push({ + date: d, + score: foundData ? Number(foundData.total) : 0, + }); + } + } else { + // 🟒 μ›”κ°„: 이번 달 포함 졜근 6κ°œμ›” + for (let i = 5; i >= 0; i--) { + const d = new Date(today); + d.setDate(1); // 1일둜 μ„€μ •ν•˜μ—¬ μ›” 계산 였차 λ°©μ§€ + d.setMonth(today.getMonth() - i); + + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, "0"); + const monthKey = `${year}-${month}`; // YYYY-MM + + // ν•΄λ‹Ή μ›”(YYYY-MM)둜 μ‹œμž‘ν•˜λŠ” 데이터 μ°ΎκΈ° + // μ›”κ°„ 데이터가 μ—¬λŸ¬ 개라면 κ·Έ 쀑 ν•˜λ‚˜(보톡 월말 κ²°μ‚°)λ₯Ό κ°€μ Έμ˜€κ±°λ‚˜ 평균을 λ‚΄μ•Ό 함 + // ν˜„μž¬λŠ” ν•΄λ‹Ή μ›”μ˜ 데이터가 있으면 κ°€μ Έμ˜€λŠ” 방식 + const foundData = trendData.find((item) => + item.date.startsWith(monthKey) + ); + + result.push({ + date: d, + score: foundData ? Number(foundData.total) : 0, + }); + } + } + + return result; + }; + + const graphData = getProcessedGraphData(); + + // 차트 데이터 생성 + const chartData = { + labels: + graphData.length > 0 + ? graphData.map((item) => { + const date = item.date; + + if (period === "monthly") { + return `${date.getMonth() + 1}μ›”`; + } else if (period === "weekly") { + return `${date.getMonth() + 1}/${date.getDate()}`; + } else { + return `${date.getMonth() + 1}/${date.getDate()}`; + } + }) + : [""], + datasets: [ + { + data: + graphData.length > 0 + ? graphData.map((item) => { + const value = item.score; + return isNaN(value) ? 0 : value; + }) + : [0], + color: (opacity = 1) => `rgba(227, 255, 124, ${opacity})`, + strokeWidth: 3, + }, + ], + }; + + // 졜근 점수 (데이터가 μžˆλŠ” κ°€μž₯ λ§ˆμ§€λ§‰ λ‚ μ§œ κΈ°μ€€) + const latestScore = + trendData.length > 0 + ? Math.round(trendData[trendData.length - 1].total || 0) + : 0; + + // 평균 점수 (전체 기둝 κΈ°μ€€) + const averageScore = + trendData.length > 0 + ? Math.round( + trendData.reduce((sum, item) => { + const value = Number(item.total); + return sum + (isNaN(value) ? 0 : value); + }, 0) / trendData.length + ) + : 0; + + return ( + + {/* 헀더 */} + + navigation.goBack()} + > + + + κ±΄κ°•μ μˆ˜ 좔이 + + + + + {/* κΈ°κ°„ 선택 νƒ­ */} + + setPeriod("daily")} + > + + 일간 + + + setPeriod("weekly")} + > + + μ£Όκ°„ + + + setPeriod("monthly")} + > + + μ›”κ°„ + + + + + {loading ? ( + + + λΆˆλŸ¬μ˜€λŠ” 쀑... + + ) : ( + /* 데이터 μœ λ¬΄μ™€ 상관없이 λ‘œλ”©μ΄ λλ‚˜λ©΄ κ·Έλž˜ν”„λ₯Ό λ³΄μ—¬μ€λ‹ˆλ‹€ (0점 처리됨) */ + <> + {/* μš”μ•½ μΉ΄λ“œ */} + + + 졜근 점수 + {latestScore} + + + 평균 점수 + {averageScore} + + + + {/* κ·Έλž˜ν”„ */} + + `rgba(227, 255, 124, ${opacity})`, + labelColor: (opacity = 1) => + `rgba(255, 255, 255, ${opacity})`, + style: { + borderRadius: 16, + }, + propsForDots: { + r: "4", + strokeWidth: "2", + stroke: "#E3FF7C", + }, + }} + bezier + style={styles.chart} + fromZero={true} + /> + + + {/* 점수 νžˆμŠ€ν† λ¦¬ (전체 기둝) */} + + 점수 기둝 + {trendData.length > 0 ? ( + trendData + .slice() + .reverse() + .map((item, index) => { + const score = Math.round(item.total || 0); + return ( + + {item.date} + {score}점 + + ); + }) + ) : ( + + + 아직 기둝된 μ μˆ˜κ°€ μ—†μŠ΅λ‹ˆλ‹€. + + + 였늘의 식단과 μš΄λ™μ„ κΈ°λ‘ν•΄λ³΄μ„Έμš”! + + + )} + + + )} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#1c1c1c", + }, + header: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + paddingHorizontal: 20, + paddingVertical: 16, + }, + backButton: { + width: 40, + height: 40, + justifyContent: "center", + }, + headerTitle: { + fontSize: 18, + fontWeight: "600", + color: "#ffffff", + }, + headerRight: { + width: 40, + }, + content: { + flex: 1, + paddingHorizontal: 20, + }, + periodTabs: { + flexDirection: "row", + backgroundColor: "#2a2a2a", + borderRadius: 12, + padding: 4, + marginBottom: 20, + }, + periodTab: { + flex: 1, + paddingVertical: 10, + alignItems: "center", + borderRadius: 8, + }, + periodTabActive: { + backgroundColor: "#E3FF7C", + }, + periodTabText: { + fontSize: 14, + fontWeight: "600", + color: "#666", + }, + periodTabTextActive: { + color: "#000", + }, + loadingContainer: { + paddingVertical: 60, + alignItems: "center", + gap: 12, + }, + loadingText: { + fontSize: 14, + color: "#aaa", + }, + emptyContainer: { + paddingVertical: 60, + alignItems: "center", + gap: 12, + }, + emptyText: { + fontSize: 16, + fontWeight: "600", + color: "#ffffff", + }, + emptySubText: { + fontSize: 14, + color: "#aaa", + }, + summaryCards: { + flexDirection: "row", + gap: 12, + marginBottom: 20, + }, + summaryCard: { + flex: 1, + backgroundColor: "#2a2a2a", + borderRadius: 12, + padding: 16, + alignItems: "center", + }, + summaryLabel: { + fontSize: 12, + color: "#aaa", + marginBottom: 8, + }, + summaryValue: { + fontSize: 32, + fontWeight: "700", + color: "#E3FF7C", + }, + chartContainer: { + alignItems: "center", + marginBottom: 20, + }, + chart: { + borderRadius: 16, + }, + historySection: { + backgroundColor: "#2a2a2a", + borderRadius: 12, + padding: 16, + marginBottom: 20, + }, + historyTitle: { + fontSize: 16, + fontWeight: "600", + color: "#ffffff", + marginBottom: 16, + }, + historyItem: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingVertical: 12, + borderBottomWidth: 1, + borderBottomColor: "#333", + }, + historyDate: { + fontSize: 14, + color: "#aaa", + }, + historyScore: { + fontSize: 16, + fontWeight: "600", + color: "#E3FF7C", + }, + emptyHistoryContainer: { + paddingVertical: 20, + alignItems: "center", + gap: 4, + }, + historyEmptyText: { + color: "#fff", + fontSize: 14, + fontWeight: "600", + textAlign: "center", + }, + historyEmptySubText: { + color: "#666", + fontSize: 12, + textAlign: "center", + }, +}); + +export default HealthScoreTrendScreen; diff --git a/src/screens/diet/TempMealRecommendScreen.tsx b/src/screens/diet/TempMealRecommendScreen.tsx index 219f1fc..572cc03 100644 --- a/src/screens/diet/TempMealRecommendScreen.tsx +++ b/src/screens/diet/TempMealRecommendScreen.tsx @@ -343,7 +343,7 @@ const TempMealRecommendScreen: React.FC = () => { protein: 0, fat: 0, }); - + const [selectedFoods, setSelectedFoods] = useState>(new Set()); const fadeAnim = useRef(new Animated.Value(0)).current; useEffect(() => { @@ -414,6 +414,18 @@ const TempMealRecommendScreen: React.FC = () => { } }; + const toggleFoodPreference = (foodName: string) => { + setSelectedFoods((prev) => { + const newSet = new Set(prev); + if (newSet.has(foodName)) { + newSet.delete(foodName); + } else { + newSet.add(foodName); + } + return newSet; + }); + }; + const handleCancelLoading = () => { Alert.alert("μš”μ²­ μ·¨μ†Œ", "식단 μΆ”μ²œ μš”μ²­μ„ μ·¨μ†Œν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?", [ { text: "계속 기닀리기", style: "cancel" }, @@ -499,7 +511,7 @@ const TempMealRecommendScreen: React.FC = () => { protein: Math.round(totalProt), fat: Math.round(totalFat), }); - + setSelectedFoods(new Set()); setScreen("result"); setCurrentDayTab(0); @@ -531,11 +543,28 @@ const TempMealRecommendScreen: React.FC = () => { try { setLoading(true); console.log("πŸ’Ύ 식단 μ €μž₯ μš”μ²­ (Server Commit)"); + + // βœ… 1. 식단 μ €μž₯ await recommendedMealAPI.saveTempMealPlan(); + // βœ… 2. μ„ ν˜Έ μŒμ‹μ΄ 있으면 μΆ”κ°€ + if (selectedFoods.size > 0 && userId) { + try { + const foodsArray = Array.from(selectedFoods); + console.log("πŸ’š μ„ ν˜Έ μŒμ‹ μ €μž₯:", foodsArray); + await userPreferencesAPI.addPreferences(userId, foodsArray); + console.log("βœ… μ„ ν˜Έ μŒμ‹ μ €μž₯ μ™„λ£Œ"); + } catch (prefError) { + console.error("μ„ ν˜Έ μŒμ‹ μ €μž₯ μ‹€νŒ¨:", prefError); + // μ„ ν˜Έ μŒμ‹ μ €μž₯ μ‹€νŒ¨ν•΄λ„ 식단 μ €μž₯은 μ„±κ³΅ν–ˆμœΌλ―€λ‘œ 계속 μ§„ν–‰ + } + } + Alert.alert( "μ €μž₯ μ™„λ£Œ! πŸŽ‰", - "AI μΆ”μ²œ 식단이 κΈ°λ‘λ˜μ—ˆμŠ΅λ‹ˆλ‹€.\nκΈ°λ‘ν•˜κΈ° ν™”λ©΄μ—μ„œ ν™•μΈν•˜μ„Έμš”.", + selectedFoods.size > 0 + ? `AI μΆ”μ²œ 식단이 κΈ°λ‘λ˜μ—ˆμŠ΅λ‹ˆλ‹€.\n${selectedFoods.size}개의 μ„ ν˜Έ μŒμ‹λ„ μΆ”κ°€λ˜μ—ˆμŠ΅λ‹ˆλ‹€.` + : "AI μΆ”μ²œ 식단이 κΈ°λ‘λ˜μ—ˆμŠ΅λ‹ˆλ‹€.\nκΈ°λ‘ν•˜κΈ° ν™”λ©΄μ—μ„œ ν™•μΈν•˜μ„Έμš”.", [ { text: "확인", @@ -943,7 +972,6 @@ const TempMealRecommendScreen: React.FC = () => { ); } - return ( { + {/* 상단 μš”μΌ νƒ­ */} { showsVerticalScrollIndicator={false} contentContainerStyle={styles.resultContainer} > + {/* μ˜μ–‘ 정보 μΉ΄λ“œ */} { + {/* 식단 리슀트 λ Œλ”λ§ */} {recommendedMeals.map((meal, index) => ( { + {/* μŒμ‹ λͺ©λ‘ */} - {meal.foods.map((food, fIdx) => ( - - { + const isSelected = selectedFoods.has(food.foodName); + + return ( + toggleFoodPreference(food.foodName)} + activeOpacity={0.7} > - - {food.foodName} {food.servingSize}g - - - ({food.calories}kcal) - - - - ))} + + {/* 1. μŒμ‹ 이름 & μš©λŸ‰ */} + + {food.foodName} {food.servingSize}g + + + {/* 2. 칼둜리 */} + + ({food.calories}kcal) + + + + + + ); + })} ))} + {/* μ„ ν˜Έ μŒμ‹ 정보 ν‘œμ‹œ (리슀트 ν•˜λ‹¨) */} + {selectedFoods.size > 0 && ( + + + + + {selectedFoods.size}개의 μŒμ‹μ„ μ„ ν˜Έ μ‹λ‹¨μœΌλ‘œ μ„ νƒν–ˆμŠ΅λ‹ˆλ‹€ + + + + )} + + {/* ν•˜λ‹¨ λ²„νŠΌ μ•‘μ…˜ */} { style={styles.saveButtonGradient} > - 이 식단 μ €μž₯ν•˜κΈ° + + {selectedFoods.size > 0 + ? `이 식단 μ €μž₯ν•˜κΈ° (μ„ ν˜Έ μŒμ‹ ${selectedFoods.size}개)` + : "이 식단 μ €μž₯ν•˜κΈ°"} + @@ -1337,6 +1415,26 @@ const styles = StyleSheet.create({ contentWrapper: { flex: 1 }, contentContainer: { paddingHorizontal: 20, paddingBottom: 40 }, resultContainer: { paddingHorizontal: 20, paddingBottom: 40, paddingTop: 10 }, + // βœ… μ„ ν˜Έ μŒμ‹ μ•ˆλ‚΄ μŠ€νƒ€μΌ μΆ”κ°€ + selectedFoodsInfo: { + marginBottom: 16, + borderRadius: 12, + overflow: "hidden", + }, + selectedFoodsInfoGradient: { + flexDirection: "row", + alignItems: "center", + padding: 12, + gap: 8, + borderWidth: 1, + borderColor: "rgba(227,255,124,0.3)", + borderRadius: 12, + }, + selectedFoodsText: { + fontSize: 14, + fontWeight: "600", + color: "#e3ff7c", + }, header: { flexDirection: "row", diff --git a/src/screens/main/MyPageScreen.tsx b/src/screens/main/MyPageScreen.tsx index fc6ad29..73f3760 100644 --- a/src/screens/main/MyPageScreen.tsx +++ b/src/screens/main/MyPageScreen.tsx @@ -1,757 +1,723 @@ -// 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"} - - - - - - - - - navigation.navigate("PaymentSuccess")} - > - - βœ… 결제 성곡 ν™”λ©΄ - - - - - - - navigation.navigate("PaymentFail")} - > - - ❌ 결제 μ‹€νŒ¨ ν™”λ©΄ - - - - - - - navigation.navigate("PaymentCancel")} - > - - 🚫 결제 μ·¨μ†Œ ν™”λ©΄ - - - - - - - - - {/* ꡬ독/결제 */} - - πŸ’³ ꡬ독/결제 - - 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 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;