diff --git a/src/components/modals/ProfileEditModal.tsx b/src/components/modals/ProfileEditModal.tsx index f4ac59c..dea6964 100644 --- a/src/components/modals/ProfileEditModal.tsx +++ b/src/components/modals/ProfileEditModal.tsx @@ -1,807 +1,1063 @@ -import React, { useState, useEffect } from "react"; -import { - View, - Text, - Modal, - TouchableOpacity, - StyleSheet, - ScrollView, - TextInput, - Alert, - ActivityIndicator, - KeyboardAvoidingView, - Platform, - StatusBar, -} from "react-native"; -import { Picker } from "@react-native-picker/picker"; -import { Ionicons as Icon } from "@expo/vector-icons"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { authAPI } from "../../services"; -import PasswordChangeModal from "./PasswordChangeModal"; - -interface ProfileEditModalProps { - isOpen: boolean; - onClose: () => void; - profileData: any; // 부모로부터 받은 프로필 데이터 - onProfileUpdate: () => void; // 저장 후 부모 컴포넌트 새로고침 -} - -const ProfileEditModal: React.FC = ({ - isOpen, - onClose, - profileData: initialProfileData, - onProfileUpdate, -}) => { - const insets = useSafeAreaInsets(); - const [loading, setLoading] = useState(false); - - // 비밀번호 변경 모달 state - const [isPasswordChangeModalOpen, setIsPasswordChangeModalOpen] = - useState(false); - - // 편집 가능한 필드들 - const [profileData, setProfileData] = useState({ - name: "", - height: "", - weight: "", - weightGoal: "", - gender: "" as "M" | "F" | "", - healthGoal: "", - workoutDaysPerWeek: "", - }); - - // 읽기 전용 필드들 - const [readOnlyData, setReadOnlyData] = useState({ - email: "", - userId: "", - birthDate: "", - }); - - const [editingField, setEditingField] = useState(null); - const [tempValue, setTempValue] = useState(""); - - // 초기 데이터 설정 - useEffect(() => { - if (initialProfileData && isOpen) { - setProfileData({ - name: initialProfileData.name || "", - height: String(initialProfileData.height || ""), - weight: String(initialProfileData.weight || ""), - weightGoal: String(initialProfileData.weightGoal || ""), - gender: initialProfileData.gender || "", - healthGoal: initialProfileData.healthGoal || "", - workoutDaysPerWeek: initialProfileData.workoutDaysPerWeek || "", - }); - - setReadOnlyData({ - email: initialProfileData.email || "", - userId: initialProfileData.userId || "", - birthDate: initialProfileData.birthDate || "", - }); - } - }, [initialProfileData, isOpen]); - - // handleFieldClick 수정 (비밀번호 모달 열기) - const handleFieldClick = (field: string) => { - if (field === "password") { - setIsPasswordChangeModalOpen(true); // 비밀번호 모달 열기 - return; - } - setEditingField(field); - setTempValue((profileData as any)[field] || ""); - }; - - const handleSave = () => { - if (editingField) { - setProfileData((prev) => ({ - ...prev, - [editingField]: tempValue, - })); - } - setEditingField(null); - setTempValue(""); - }; - - // 프로필 저장 (API 호출) - const handleSaveProfile = async () => { - try { - setLoading(true); - - // API로 전송할 데이터 준비 - const updateData = { - name: profileData.name, - height: Number(profileData.height), - weight: Number(profileData.weight), - weightGoal: Number(profileData.weightGoal), - gender: profileData.gender as "M" | "F", - healthGoal: profileData.healthGoal, - workoutDaysPerWeek: profileData.workoutDaysPerWeek, - }; - - // API 호출 - const response = await authAPI.updateProfile(updateData); - - if (response.success) { - Alert.alert("성공", "프로필이 수정되었습니다."); - onProfileUpdate(); // 부모 컴포넌트에 새로고침 요청 - onClose(); - } else { - Alert.alert("오류", response.message || "프로필 수정에 실패했습니다."); - } - } catch (error: any) { - console.error("프로필 수정 실패:", error); - Alert.alert("오류", error.message || "프로필 수정에 실패했습니다."); - } finally { - setLoading(false); - } - }; - - // 성별을 한글로 표시 - const getGenderText = (gender: string) => { - return gender === "M" ? "남성" : gender === "F" ? "여성" : ""; - }; - - // 운동 목표를 한글로 표시 - const getHealthGoalText = (goal: string) => { - switch (goal) { - case "DIET": - return "다이어트"; - case "BULK": - return "벌크업"; - case "LEAN_MASS": - return "린매스"; - case "MUSCLE_GAIN": - return "근육 증가"; - case "MAINTENANCE": - return "유지"; - default: - return goal; - } - }; - - return ( - <> - - - - - - - - - 정보 수정 - { - handleSave(); - handleSaveProfile(); - }} - style={styles.navBtn} - disabled={loading} - > - {loading ? ( - - ) : ( - - 저장 - - )} - - - - - - - - - - {profileData.name}님 - - - - - - - - {/* 이름 */} - {editingField === "name" ? ( - - 이름 - - - - - - - - ) : ( - handleFieldClick("name")} - > - 이름 - - {profileData.name} - - - - )} - - {/* 이메일 (읽기 전용) */} - - 이메일 - - {readOnlyData.email} - - - - {/* 아이디 (읽기 전용) */} - - 아이디 - - {readOnlyData.userId} - - - - {/* 비밀번호 */} - handleFieldClick("password")} - > - 비밀번호 - - - 재설정 - - - - - - {/* 생년월일 (읽기 전용) */} - - 생년월일 - - {readOnlyData.birthDate} - - - - - - {/* 성별 */} - {editingField === "gender" ? ( - - 성별 - - - - {/* 텍스트 색상을 검은색으로 변경 */} - - - - - - - - - - ) : ( - handleFieldClick("gender")} - > - 성별 - - - {getGenderText(profileData.gender)} - - - - - )} - - {/* 신체정보 (키, 몸무게) */} - {editingField === "bodyInfo" ? ( - - 신체정보 - - - setProfileData((prev) => ({ ...prev, height: text })) - } - placeholder="키" - keyboardType="numeric" - placeholderTextColor={NEW_COLORS.placeholder} - /> - cm - - setProfileData((prev) => ({ ...prev, weight: text })) - } - placeholder="몸무게" - keyboardType="numeric" - placeholderTextColor={NEW_COLORS.placeholder} - /> - kg - setEditingField(null)} - style={styles.saveEditBtn} - > - - - - - ) : ( - setEditingField("bodyInfo")} - > - 신체정보 - - - {profileData.height}cm / {profileData.weight}kg - - - - - )} - - {/* 목표 체중 */} - {editingField === "weightGoal" ? ( - - 목표 체중 - - - kg - - - - - - ) : ( - handleFieldClick("weightGoal")} - > - 목표 체중 - - - {profileData.weightGoal}kg - - - - - )} - - {/* 운동 목표 */} - {editingField === "healthGoal" ? ( - - 운동 목표 - - - - {/* 텍스트 색상을 검은색으로 변경 */} - - - - - - - - - - - - - ) : ( - handleFieldClick("healthGoal")} - > - 운동 목표 - - - {getHealthGoalText(profileData.healthGoal)} - - - - - )} - - {/* 주간 운동 일수 */} - {editingField === "workoutDaysPerWeek" ? ( - - 주간 운동 일수 - - - - - - - - ) : ( - handleFieldClick("workoutDaysPerWeek")} - > - 주간 운동 일수 - - - {profileData.workoutDaysPerWeek || "설정 안 됨"} - - - - - )} - - - - - - - {/* 비밀번호 변경 모달 */} - setIsPasswordChangeModalOpen(false)} - /> - - ); -}; - -const NEW_COLORS = { - background: "#1a1a1a", - card_bg: "#252525", - text: "#ffffff", - text_secondary: "#a0a0a0", - accent: "#e3ff7c", - placeholder: "#777777", - border_dark: "#333333", - border_light: "#404040", -}; - -const styles = StyleSheet.create({ - overlay: { - flex: 1, - backgroundColor: NEW_COLORS.background, - }, - modalContent: { - flex: 1, - backgroundColor: NEW_COLORS.background, - }, - nav: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - paddingBottom: 16, - paddingHorizontal: 20, - minHeight: 56, - borderBottomWidth: 1, - borderBottomColor: NEW_COLORS.border_dark, - backgroundColor: NEW_COLORS.background, - position: "relative", - }, - navBtn: { - padding: 4, - minWidth: 40, - justifyContent: "center", - alignItems: "center", - }, - navBtnText: { - fontSize: 16, - color: NEW_COLORS.text, - fontWeight: "600", - }, - navTitle: { - position: "absolute", - left: "50%", - transform: [{ translateX: -50 }], - fontSize: 18, - fontWeight: "700", - color: NEW_COLORS.text, - }, - body: { - flex: 1, - }, - bodyContent: { - paddingBottom: 100, - }, - profileSection: { - flexDirection: "row", - alignItems: "center", - padding: 24, - paddingHorizontal: 20, - gap: 16, - }, - avatar: { - width: 60, - height: 60, - borderRadius: 30, - backgroundColor: NEW_COLORS.border_dark, - justifyContent: "center", - alignItems: "center", - flexShrink: 0, - }, - profileInfo: { - flexDirection: "row", - alignItems: "center", - gap: 12, - }, - profileName: { - fontSize: 20, - fontWeight: "700", - color: NEW_COLORS.text, - }, - editAvatarBtn: { - padding: 8, - borderRadius: 15, - backgroundColor: NEW_COLORS.card_bg, - }, - infoList: { - paddingHorizontal: 20, - }, - infoItem: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - paddingVertical: 18, - borderBottomWidth: 1, - borderBottomColor: NEW_COLORS.border_dark, - }, - infoItemLast: { - borderBottomWidth: 0, - }, - infoLabel: { - fontSize: 16, - color: NEW_COLORS.text_secondary, - fontWeight: "500", - minWidth: 100, - flexShrink: 0, - }, - infoValue: { - fontSize: 16, - color: NEW_COLORS.text, - fontWeight: "400", - }, - readOnlyValue: { - color: NEW_COLORS.text_secondary, - fontWeight: "400", - }, - infoValueWithArrow: { - flexDirection: "row", - alignItems: "center", - gap: 4, - }, - editingItem: { - backgroundColor: NEW_COLORS.card_bg, - borderRadius: 8, - paddingVertical: 12, - paddingHorizontal: 16, - marginHorizontal: -20, - borderBottomWidth: 0, - }, - editControls: { - flexDirection: "row", - alignItems: "center", - gap: 8, - flex: 1, - justifyContent: "flex-end", - }, - editInput: { - backgroundColor: NEW_COLORS.border_dark, - borderWidth: 1, - borderColor: NEW_COLORS.border_light, - borderRadius: 6, - padding: 10, - paddingHorizontal: 12, - fontSize: 15, - color: NEW_COLORS.text, - minWidth: 120, - flex: 1, - }, - bodyInfoControls: { - gap: 6, - flexWrap: "nowrap", - }, - bodyInput: { - minWidth: 50, - maxWidth: 65, - textAlign: "center", - fontSize: 14, - padding: 8, - }, - unit: { - fontSize: 15, - color: NEW_COLORS.text_secondary, - }, - saveEditBtn: { - width: 32, - height: 32, - borderRadius: 6, - backgroundColor: NEW_COLORS.accent, - justifyContent: "center", - alignItems: "center", - flexShrink: 0, - }, - saveEditBtnText: { - fontSize: 18, - color: NEW_COLORS.text, - }, - pickerContainer: { - backgroundColor: NEW_COLORS.border_dark, - borderWidth: 1, - borderColor: NEW_COLORS.border_light, - borderRadius: 6, - flex: 1, - minWidth: 120, - height: 40, - overflow: "hidden", - justifyContent: "center", - }, - picker: { - color: NEW_COLORS.text, - height: 40, - backgroundColor: "transparent", - }, - pickerItem: { - color: "black", - fontSize: 15, - }, -}); - -export default ProfileEditModal; +import React, { useState, useEffect } from "react"; +import { + View, + Text, + Modal, + TouchableOpacity, + StyleSheet, + ScrollView, + TextInput, + Alert, + ActivityIndicator, + KeyboardAvoidingView, + Platform, + StatusBar, +} from "react-native"; +import { Ionicons as Icon } from "@expo/vector-icons"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { authAPI } from "../../services"; +import PasswordChangeModal from "./PasswordChangeModal"; + +interface ProfileEditModalProps { + isOpen: boolean; + onClose: () => void; + profileData: any; + onProfileUpdate: () => void; +} + +const ProfileEditModal: React.FC = ({ + isOpen, + onClose, + profileData: initialProfileData, + onProfileUpdate, +}) => { + const insets = useSafeAreaInsets(); + const [loading, setLoading] = useState(false); + const [isPasswordChangeModalOpen, setIsPasswordChangeModalOpen] = + useState(false); + + const [profileData, setProfileData] = useState({ + name: "", + height: "", + weight: "", + weightGoal: "", + gender: "" as "M" | "F" | "", + healthGoal: "", + workoutDaysPerWeek: "", + }); + + const [readOnlyData, setReadOnlyData] = useState({ + email: "", + userId: "", + birthDate: "", + }); + + const [editingField, setEditingField] = useState(null); + const [tempValue, setTempValue] = useState(""); + + useEffect(() => { + if (initialProfileData && isOpen) { + setProfileData({ + name: initialProfileData.name || "", + height: String(initialProfileData.height || ""), + weight: String(initialProfileData.weight || ""), + weightGoal: String(initialProfileData.weightGoal || ""), + gender: initialProfileData.gender || "", + healthGoal: initialProfileData.healthGoal || "", + workoutDaysPerWeek: initialProfileData.workoutDaysPerWeek || "", + }); + + setReadOnlyData({ + email: initialProfileData.email || "", + userId: initialProfileData.userId || "", + birthDate: initialProfileData.birthDate || "", + }); + } + }, [initialProfileData, isOpen]); + + const handleFieldClick = (field: string) => { + if (field === "password") { + setIsPasswordChangeModalOpen(true); + return; + } + setEditingField(field); + setTempValue((profileData as any)[field] || ""); + }; + + const handleSave = () => { + if (editingField) { + setProfileData((prev) => ({ + ...prev, + [editingField]: tempValue, + })); + } + setEditingField(null); + setTempValue(""); + }; + + const handleCancel = () => { + setEditingField(null); + setTempValue(""); + }; + + const handleSaveProfile = async () => { + try { + setLoading(true); + + const updateData = { + name: profileData.name, + height: Number(profileData.height), + weight: Number(profileData.weight), + weightGoal: Number(profileData.weightGoal), + gender: profileData.gender as "M" | "F", + healthGoal: profileData.healthGoal, + workoutDaysPerWeek: profileData.workoutDaysPerWeek, + }; + + const response = await authAPI.updateProfile(updateData); + + if (response.success) { + Alert.alert("성공", "프로필이 수정되었습니다."); + onProfileUpdate(); + onClose(); + } else { + Alert.alert("오류", response.message || "프로필 수정에 실패했습니다."); + } + } catch (error: any) { + console.error("프로필 수정 실패:", error); + Alert.alert("오류", error.message || "프로필 수정에 실패했습니다."); + } finally { + setLoading(false); + } + }; + + const getGenderText = (gender: string) => { + return gender === "M" ? "남성" : gender === "F" ? "여성" : "선택 안 됨"; + }; + + const getHealthGoalText = (goal: string) => { + switch (goal) { + case "DIET": + return "다이어트"; + case "BULK": + return "벌크업"; + case "LEAN_MASS": + return "린매스"; + case "MUSCLE_GAIN": + return "근육 증가"; + case "MAINTENANCE": + return "유지"; + default: + return "선택 안 됨"; + } + }; + + // 성별 선택 옵션 렌더링 + const renderGenderSelector = () => ( + + 성별을 선택하세요 + + setTempValue("M")} + > + + + 남성 + + + setTempValue("F")} + > + + + 여성 + + + + + + 취소 + + + 확인 + + + + ); + + // 운동 목표 선택 옵션 렌더링 + const renderHealthGoalSelector = () => { + const goals = [ + { label: "다이어트", value: "DIET", icon: "flame" }, + { label: "벌크업", value: "BULK", icon: "barbell" }, + { label: "린매스", value: "LEAN_MASS", icon: "fitness" }, + { label: "근육 증가", value: "MUSCLE_GAIN", icon: "body" }, + { label: "유지", value: "MAINTENANCE", icon: "heart" }, + ]; + + return ( + + 운동 목표를 선택하세요 + + {goals.map((goal) => ( + setTempValue(goal.value)} + > + + + {goal.label} + + + ))} + + + + 취소 + + + 확인 + + + + ); + }; + + return ( + <> + + + + + {/* 헤더 */} + + + + + 정보 수정 + { + handleSave(); + handleSaveProfile(); + }} + style={styles.navBtn} + disabled={loading} + > + {loading ? ( + + ) : ( + 저장 + )} + + + + + {/* 프로필 섹션 */} + + + + + + {profileData.name}님 + + + 사진 변경 + + + + + {/* 정보 목록 */} + + {/* 이름 */} + {editingField === "name" ? ( + + 이름 + + + + 취소 + + + 확인 + + + + ) : ( + handleFieldClick("name")} + activeOpacity={0.7} + > + + + 이름 + + + {profileData.name} + + + + )} + + {/* 이메일 (읽기 전용) */} + + + + 이메일 + + {readOnlyData.email} + + + {/* 아이디 (읽기 전용) */} + + + + 아이디 + + + {readOnlyData.userId} + + + + {/* 비밀번호 */} + handleFieldClick("password")} + activeOpacity={0.7} + > + + + 비밀번호 + + + 변경하기 + + + + + {/* 생년월일 (읽기 전용) */} + + + + 생년월일 + + + {readOnlyData.birthDate} + + + + {/* 성별 */} + {editingField === "gender" ? ( + + {renderGenderSelector()} + + ) : ( + handleFieldClick("gender")} + activeOpacity={0.7} + > + + + 성별 + + + + {getGenderText(profileData.gender)} + + + + + )} + + {/* 신체정보 */} + {editingField === "bodyInfo" ? ( + + 신체정보 + + + + + + setProfileData((prev) => ({ + ...prev, + height: text, + })) + } + placeholder="0" + keyboardType="numeric" + placeholderTextColor={NEW_COLORS.placeholder} + /> + cm + + + + 몸무게 + + + setProfileData((prev) => ({ + ...prev, + weight: text, + })) + } + placeholder="0" + keyboardType="numeric" + placeholderTextColor={NEW_COLORS.placeholder} + /> + kg + + + + + + 취소 + + setEditingField(null)} + style={styles.confirmButton} + > + 확인 + + + + ) : ( + setEditingField("bodyInfo")} + activeOpacity={0.7} + > + + + 신체정보 + + + + {profileData.height}cm / {profileData.weight}kg + + + + + )} + + {/* 목표 체중 */} + {editingField === "weightGoal" ? ( + + 목표 체중 + + + kg + + + + 취소 + + + 확인 + + + + ) : ( + handleFieldClick("weightGoal")} + activeOpacity={0.7} + > + + + 목표 체중 + + + + {profileData.weightGoal}kg + + + + + )} + + {/* 운동 목표 */} + {editingField === "healthGoal" ? ( + + {renderHealthGoalSelector()} + + ) : ( + handleFieldClick("healthGoal")} + activeOpacity={0.7} + > + + + 운동 목표 + + + + {getHealthGoalText(profileData.healthGoal)} + + + + + )} + + {/* 주간 운동 일수 */} + {editingField === "workoutDaysPerWeek" ? ( + + 주간 운동 일수 + + + + 취소 + + + 확인 + + + + ) : ( + handleFieldClick("workoutDaysPerWeek")} + activeOpacity={0.7} + > + + + 주간 운동 일수 + + + + {profileData.workoutDaysPerWeek || "설정 안 됨"} + + + + + )} + + + + + + + setIsPasswordChangeModalOpen(false)} + /> + + ); +}; + +const NEW_COLORS = { + background: "#1a1a1a", + card_bg: "#252525", + text: "#ffffff", + text_secondary: "#a0a0a0", + accent: "#e3ff7c", + placeholder: "#777777", + border_dark: "#333333", + border_light: "#404040", +}; + +const styles = StyleSheet.create({ + overlay: { + flex: 1, + backgroundColor: NEW_COLORS.background, + }, + modalContent: { + flex: 1, + backgroundColor: NEW_COLORS.background, + }, + nav: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingBottom: 16, + paddingHorizontal: 20, + minHeight: 56, + borderBottomWidth: 1, + borderBottomColor: NEW_COLORS.border_dark, + backgroundColor: NEW_COLORS.background, + }, + navBtn: { + padding: 8, + minWidth: 60, + justifyContent: "center", + alignItems: "center", + }, + navTitle: { + fontSize: 18, + fontWeight: "700", + color: NEW_COLORS.text, + position: "absolute", + left: 0, + right: 0, + textAlign: "center", + }, + saveText: { + fontSize: 16, + color: NEW_COLORS.accent, + fontWeight: "600", + }, + body: { + flex: 1, + }, + bodyContent: { + paddingBottom: 100, + }, + profileSection: { + flexDirection: "row", + alignItems: "center", + padding: 24, + gap: 16, + backgroundColor: NEW_COLORS.card_bg, + marginHorizontal: 20, + marginTop: 20, + marginBottom: 24, + borderRadius: 16, + }, + avatar: { + width: 70, + height: 70, + borderRadius: 35, + backgroundColor: NEW_COLORS.border_dark, + justifyContent: "center", + alignItems: "center", + }, + profileInfo: { + flex: 1, + gap: 8, + }, + profileName: { + fontSize: 22, + fontWeight: "700", + color: NEW_COLORS.text, + }, + editAvatarBtn: { + flexDirection: "row", + alignItems: "center", + gap: 6, + paddingVertical: 6, + paddingHorizontal: 12, + backgroundColor: NEW_COLORS.background, + borderRadius: 20, + alignSelf: "flex-start", + }, + editAvatarText: { + fontSize: 13, + color: NEW_COLORS.accent, + fontWeight: "600", + }, + infoList: { + paddingHorizontal: 20, + gap: 12, + }, + infoItem: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingVertical: 16, + paddingHorizontal: 16, + backgroundColor: NEW_COLORS.card_bg, + borderRadius: 12, + minHeight: 60, + }, + infoItemLast: { + marginBottom: 0, + }, + readOnlyItem: { + opacity: 0.7, + }, + infoLeft: { + flexDirection: "row", + alignItems: "center", + gap: 12, + flex: 1, + }, + infoLabel: { + fontSize: 16, + color: NEW_COLORS.text, + fontWeight: "500", + }, + infoRight: { + flexDirection: "row", + alignItems: "center", + gap: 8, + }, + infoValue: { + fontSize: 16, + color: NEW_COLORS.text, + fontWeight: "400", + }, + readOnlyValue: { + fontSize: 15, + color: NEW_COLORS.text_secondary, + fontWeight: "400", + }, + accentValue: { + fontSize: 15, + color: NEW_COLORS.accent, + fontWeight: "600", + }, + editingCard: { + backgroundColor: NEW_COLORS.card_bg, + borderRadius: 12, + padding: 20, + gap: 16, + }, + editingLabel: { + fontSize: 16, + color: NEW_COLORS.text, + fontWeight: "600", + marginBottom: 4, + }, + fullWidthInput: { + backgroundColor: NEW_COLORS.background, + borderWidth: 1, + borderColor: NEW_COLORS.border_light, + borderRadius: 10, + padding: 14, + fontSize: 16, + color: NEW_COLORS.text, + }, + inlineInputWrapper: { + flexDirection: "row", + alignItems: "center", + backgroundColor: NEW_COLORS.background, + borderWidth: 1, + borderColor: NEW_COLORS.border_light, + borderRadius: 10, + paddingHorizontal: 14, + height: 50, + }, + inlineInput: { + flex: 1, + fontSize: 16, + color: NEW_COLORS.text, + padding: 0, + }, + inlineUnit: { + fontSize: 16, + color: NEW_COLORS.text_secondary, + marginLeft: 8, + }, + bodyInfoInputs: { + flexDirection: "row", + gap: 12, + }, + bodyInputGroup: { + flex: 1, + gap: 8, + }, + bodyInputLabel: { + fontSize: 14, + color: NEW_COLORS.text_secondary, + fontWeight: "500", + }, + bodyInputWrapper: { + flexDirection: "row", + alignItems: "center", + backgroundColor: NEW_COLORS.background, + borderWidth: 1, + borderColor: NEW_COLORS.border_light, + borderRadius: 10, + paddingHorizontal: 14, + height: 50, + }, + bodyInput: { + flex: 1, + fontSize: 16, + color: NEW_COLORS.text, + textAlign: "center", + padding: 0, + }, + bodyUnit: { + fontSize: 14, + color: NEW_COLORS.text_secondary, + }, + editActionButtons: { + flexDirection: "row", + gap: 12, + marginTop: 8, + }, + cancelButton: { + flex: 1, + paddingVertical: 14, + borderRadius: 10, + backgroundColor: NEW_COLORS.background, + borderWidth: 1, + borderColor: NEW_COLORS.border_light, + alignItems: "center", + }, + cancelButtonText: { + fontSize: 16, + color: NEW_COLORS.text, + fontWeight: "600", + }, + confirmButton: { + flex: 1, + paddingVertical: 14, + borderRadius: 10, + backgroundColor: NEW_COLORS.accent, + alignItems: "center", + }, + confirmButtonText: { + fontSize: 16, + color: NEW_COLORS.background, + fontWeight: "700", + }, + // 선택자 스타일 + selectorContainer: { + gap: 16, + }, + selectorLabel: { + fontSize: 15, + color: NEW_COLORS.text_secondary, + fontWeight: "500", + marginBottom: 4, + }, + optionButtons: { + flexDirection: "row", + gap: 12, + }, + optionButton: { + flex: 1, + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + gap: 8, + paddingVertical: 16, + borderRadius: 10, + backgroundColor: NEW_COLORS.background, + borderWidth: 2, + borderColor: NEW_COLORS.border_light, + }, + optionButtonActive: { + backgroundColor: NEW_COLORS.accent, + borderColor: NEW_COLORS.accent, + }, + optionButtonText: { + fontSize: 16, + color: NEW_COLORS.text, + fontWeight: "600", + }, + optionButtonTextActive: { + color: NEW_COLORS.background, + }, + goalGrid: { + flexDirection: "row", + flexWrap: "wrap", + gap: 12, + }, + goalOption: { + width: "48%", + flexDirection: "column", + alignItems: "center", + gap: 8, + paddingVertical: 16, + borderRadius: 10, + backgroundColor: NEW_COLORS.background, + borderWidth: 2, + borderColor: NEW_COLORS.border_light, + }, + goalOptionActive: { + backgroundColor: NEW_COLORS.accent, + borderColor: NEW_COLORS.accent, + }, + goalOptionText: { + fontSize: 15, + color: NEW_COLORS.text, + fontWeight: "600", + }, + goalOptionTextActive: { + color: NEW_COLORS.background, + }, +}); + +export default ProfileEditModal; diff --git a/src/screens/exercise/RoutineRecommendScreen.tsx b/src/screens/exercise/RoutineRecommendScreen.tsx index 05e787a..2673a6b 100644 --- a/src/screens/exercise/RoutineRecommendScreen.tsx +++ b/src/screens/exercise/RoutineRecommendScreen.tsx @@ -229,11 +229,200 @@ const RoutineRecommendScreen = ({ navigation }: any) => { }; const handleDelete = async (plan: ExercisePlan) => { - Alert.alert("알림", "서버 삭제 기능이 아직 연동되지 않았습니다."); - }; + Alert.alert("삭제 확인", `"${plan.planName}"을(를) 삭제하시겠습니까?`, [ + { + text: "취소", + style: "cancel", + }, + { + text: "삭제", + style: "destructive", + onPress: async () => { + try { + setLoading(true); + + // "나의 주간 운동 일정"인 경우 + if (plan.planId.startsWith("weekly_history")) { + // 모든 날짜 추출 + const dates = plan.days + .map((dayExercises) => dayExercises[0]?.date) + .filter(Boolean); + + if (dates.length === 0) { + Alert.alert("오류", "삭제할 날짜 정보가 없습니다."); + return; + } + + console.log(`🗑️ 총 ${dates.length}일 삭제 시작...`); + + let successCount = 0; + let failCount = 0; + + // 각 날짜를 개별적으로 삭제 + for (const date of dates) { + try { + console.log(`📅 ${date} 삭제 중...`); + const result = + await recommendedExerciseAPI.deleteRecommendedExerciseByDate( + date + ); + + // 응답에 deletedCount 체크 + if (result && result.deletedCount > 0) { + successCount++; + console.log( + `✅ ${date} 삭제 성공 (${result.deletedCount}개)` + ); + } else { + failCount++; + console.log(`⚠️ ${date} 삭제할 데이터 없음`); + } + } catch (err) { + failCount++; + console.error(`❌ ${date} 삭제 실패:`, err); + } + } + if (successCount > 0) { + Alert.alert( + "삭제 완료", + `${successCount}일치 운동이 삭제되었습니다.${ + failCount > 0 ? `\n(${failCount}일치는 삭제 실패)` : "" + }` + ); + } else { + Alert.alert( + "알림", + "삭제할 데이터가 없거나 삭제에 실패했습니다." + ); + } + } + // "오늘의 추천"인 경우 + else if (plan.planId.startsWith("daily_")) { + const today = new Date().toISOString().split("T")[0]; + console.log(`📅 ${today} 삭제 중...`); + + const result = + await recommendedExerciseAPI.deleteRecommendedExerciseByDate( + today + ); + + if (result && result.deletedCount > 0) { + Alert.alert("성공", "오늘의 추천이 삭제되었습니다."); + } else { + Alert.alert("알림", "삭제할 데이터가 없습니다."); + } + } else { + Alert.alert("알림", "이 플랜은 삭제할 수 없습니다."); + return; + } + + // 삭제 후 목록 새로고침 + await loadExercisePlans(); + } catch (error: any) { + console.error("❌ 삭제 중 오류:", error); + Alert.alert( + "오류", + error.message || "삭제 중 문제가 발생했습니다." + ); + } finally { + setLoading(false); + } + }, + }, + ]); + }; const handleBulkDelete = async () => { - Alert.alert("알림", "서버 삭제 기능이 아직 연동되지 않았습니다."); + if (selectedPlanIds.length === 0) return; + + Alert.alert( + "일괄 삭제", + `선택한 ${selectedPlanIds.length}개의 플랜을 삭제하시겠습니까?`, + [ + { + text: "취소", + style: "cancel", + }, + { + text: "삭제", + style: "destructive", + onPress: async () => { + try { + setLoading(true); + + const selectedPlans = plans.filter((p) => + selectedPlanIds.includes(p.planId) + ); + + let totalSuccess = 0; + let totalFail = 0; + + for (const plan of selectedPlans) { + if (plan.planId.startsWith("weekly_history")) { + const dates = plan.days + .map((dayExercises) => dayExercises[0]?.date) + .filter(Boolean); + + for (const date of dates) { + try { + const result = + await recommendedExerciseAPI.deleteRecommendedExerciseByDate( + date + ); + if (result && result.deletedCount > 0) { + totalSuccess++; + } else { + totalFail++; + } + } catch (err) { + totalFail++; + } + } + } else if (plan.planId.startsWith("daily_")) { + const today = new Date().toISOString().split("T")[0]; + try { + const result = + await recommendedExerciseAPI.deleteRecommendedExerciseByDate( + today + ); + if (result && result.deletedCount > 0) { + totalSuccess++; + } else { + totalFail++; + } + } catch (err) { + totalFail++; + } + } + } + + if (totalSuccess > 0) { + Alert.alert( + "삭제 완료", + `${totalSuccess}일치 운동이 삭제되었습니다.${ + totalFail > 0 ? `\n(${totalFail}일치는 삭제 실패)` : "" + }` + ); + } else { + Alert.alert("알림", "삭제할 데이터가 없습니다."); + } + + setSelectedPlanIds([]); + setIsEditMode(false); + await loadExercisePlans(); + } catch (error: any) { + console.error("❌ 일괄 삭제 중 오류:", error); + Alert.alert( + "오류", + error.message || "삭제 중 문제가 발생했습니다." + ); + } finally { + setLoading(false); + } + }, + }, + ] + ); }; const currentDayExercises = selectedPlan?.days?.[selectedDay] || []; @@ -257,9 +446,7 @@ const RoutineRecommendScreen = ({ navigation }: any) => { setIsEditMode(true); } }} - > - {isEditMode ? "완료" : "편집"} - + > )} {!plans.length && } @@ -441,25 +628,41 @@ const RoutineRecommendScreen = ({ navigation }: any) => { style={styles.dayTabsContainer} contentContainerStyle={styles.dayTabs} > - {selectedPlan.days.map((_, index) => ( - setSelectedDay(index)} - > - { + // 해당 day의 날짜 추출 (첫 번째 운동의 date 필드 사용) + const dateStr = dayExercises[0]?.date; + let displayText = `${index + 1}일차`; // 기본값 + + if (dateStr) { + // 날짜를 파싱해서 "MM/DD (요일)" 형식으로 표시 + const date = new Date(dateStr); + const month = date.getMonth() + 1; + const day = date.getDate(); + const weekdays = ["일", "월", "화", "수", "목", "금", "토"]; + const weekday = weekdays[date.getDay()]; + displayText = `${month}/${day} (${weekday})`; + } + + return ( + setSelectedDay(index)} > - {index + 1}일차 - - - ))} + + {displayText} + + + ); + })} )} @@ -475,7 +678,16 @@ const RoutineRecommendScreen = ({ navigation }: any) => { 💪 - {exercise.name} + + {exercise.name} + {exercise.target && ( + + + {exercise.target} + + + )} + {exercise.sets ? `${exercise.sets}세트 ` : ""} {exercise.reps ? `${exercise.reps}회 ` : ""} @@ -684,9 +896,47 @@ const styles = StyleSheet.create({ alignItems: "center", }, exerciseIconText: { fontSize: 32 }, - exerciseInfo: { flex: 1, gap: 6 }, - exerciseName: { fontSize: 16, fontWeight: "600", color: "#ffffff" }, - exerciseDetail: { fontSize: 14, color: "#aaaaaa" }, + exerciseInfo: { + flex: 1, + gap: 6, + }, + exerciseNameRow: { + flexDirection: "row", + alignItems: "center", + gap: 6, + flexWrap: "wrap", + }, + exerciseName: { + fontSize: 16, + fontWeight: "600", + color: "#ffffff", + }, + targetBadge: { + backgroundColor: "#4a90e2", + paddingVertical: 3, + paddingHorizontal: 8, + borderRadius: 8, + }, + targetBadgeText: { + fontSize: 11, + fontWeight: "600", + color: "#ffffff", + }, + setsBadge: { + backgroundColor: "#e3ff7c", + paddingVertical: 3, + paddingHorizontal: 8, + borderRadius: 8, + }, + setsBadgeText: { + fontSize: 11, + fontWeight: "600", + color: "#111111", + }, + exerciseDetail: { + fontSize: 14, + color: "#aaaaaa", + }, }); export default RoutineRecommendScreen; diff --git a/src/services/recommendedExerciseAPI.ts b/src/services/recommendedExerciseAPI.ts index d99e997..a8da7e7 100644 --- a/src/services/recommendedExerciseAPI.ts +++ b/src/services/recommendedExerciseAPI.ts @@ -318,7 +318,9 @@ export const recommendedExerciseAPI = { * 7. 임시 추천 운동 요약 조회 (TempSummary) * @param date - 날짜 (yyyy-MM-dd 형식) */ - getTempSummary: async (date: string): Promise<{ + getTempSummary: async ( + date: string + ): Promise<{ date: string; focus: string; durationMin: number; @@ -350,6 +352,35 @@ export const recommendedExerciseAPI = { return null as any; } + throw error; + } + }, + /** + * 8. 날짜별 추천받은 운동 삭제 + * @param date - 삭제할 날짜 (yyyy-MM-dd) + * 명세서 기준: DELETE /api/exercise-recommendations/temp?startDate=...&endDate=... + */ + deleteRecommendedExerciseByDate: async (date: string): Promise => { + try { + console.log("🗑️ 날짜별 운동 삭제 (명세서 기반 수정):", date); + + // 📸 사진에 나온 대로 startDate와 endDate를 동일하게 설정하여 요청 + const response = await request( + `/api/exercise-recommendations/temp?startDate=${date}&endDate=${date}`, + { + method: "DELETE", + } + ); + + console.log("✅ 날짜별 운동 삭제 성공:", response); + return response; + } catch (error: any) { + console.error("❌ 날짜별 운동 삭제 실패:", error); + + if (error.status === 401) { + throw new Error("로그인이 필요합니다."); + } + throw error; } },