From 0117f08919d30dc5aa6ce4e4413c74917769fa03 Mon Sep 17 00:00:00 2001 From: a06246 Date: Sat, 13 Dec 2025 04:30:19 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=EC=B9=B4=EC=B9=B4=EC=98=A4=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EA=B4=80=EB=A0=A8=20API=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/authAPI.ts | 115 ++++++++++++++++++++++++++++++++-------- 1 file changed, 94 insertions(+), 21 deletions(-) diff --git a/src/services/authAPI.ts b/src/services/authAPI.ts index d6c015e..653fb86 100644 --- a/src/services/authAPI.ts +++ b/src/services/authAPI.ts @@ -717,23 +717,23 @@ export const authAPI = { kakaoLogin: async ( code: string ): Promise<{ - success: boolean; - message: string; - accessToken?: string; - refreshToken?: string; - tokenType?: string; - expiresIn?: number; - membershipType?: "FREE" | "PREMIUM"; + accessToken: string; + refreshToken: string; + userId: number; + nickname: string; + profileImageUrl: string; + newUser: boolean; + onboarded: boolean; }> => { try { const response = await request<{ - success: boolean; - message: string; - accessToken?: string; - refreshToken?: string; - tokenType?: string; - expiresIn?: number; - membershipType?: "FREE" | "PREMIUM"; + accessToken: string; + refreshToken: string; + userId: number; + nickname: string; + profileImageUrl: string; + newUser: boolean; + onboarded: boolean; }>("/api/auth/kakao/login", { method: "POST", body: JSON.stringify({ code }), @@ -742,10 +742,16 @@ export const authAPI = { if (response.accessToken) { await AsyncStorage.setItem(ACCESS_TOKEN_KEY, response.accessToken); + // userId 저장 + if (response.userId) { + await AsyncStorage.setItem("userId", String(response.userId)); + console.log("[AUTH] 카카오 로그인 - userId 저장 완료:", response.userId); + } + + // JWT에서도 userId 추출 시도 const payload = decodeJWT(response.accessToken); - if (payload && payload.userPk) { + if (payload && payload.userPk && !response.userId) { await AsyncStorage.setItem("userId", String(payload.userPk)); - console.log("[AUTH] 카카오 로그인 - userId 저장 완료:", payload.userPk); } } @@ -753,11 +759,8 @@ export const authAPI = { await AsyncStorage.setItem(REFRESH_TOKEN_KEY, response.refreshToken); } - if (response.membershipType) { - await AsyncStorage.setItem("membershipType", response.membershipType); - } else { - await AsyncStorage.setItem("membershipType", "FREE"); - } + // membershipType은 기본값으로 설정 (백엔드 응답에 없음) + await AsyncStorage.setItem("membershipType", "FREE"); return response; } catch (error: any) { @@ -765,4 +768,74 @@ export const authAPI = { throw error; } }, + + /** + * 카카오 로그인 URL 가져오기 + * @returns 카카오 로그인 URL + */ + getKakaoLoginUrl: async (): Promise<{ url: string }> => { + return request<{ url: string }>("/api/auth/kakao/login-url", { + method: "GET", + }); + }, + + /** + * 카카오 로그아웃 + */ + kakaoLogout: async (): Promise<{ message: string }> => { + try { + const response = await request<{ message: string }>("/api/auth/kakao/logout", { + method: "POST", + }); + return response; + } catch (error: any) { + console.error("[AUTH] 카카오 로그아웃 실패:", error); + throw error; + } + }, + + /** + * 카카오 연결 해제 + */ + kakaoUnlink: async (): Promise<{ message: string }> => { + try { + const response = await request<{ message: string }>("/api/auth/kakao/unlink", { + method: "DELETE", + }); + return response; + } catch (error: any) { + console.error("[AUTH] 카카오 연결 해제 실패:", error); + throw error; + } + }, + + /** + * 카카오 로그인 후 온보딩 데이터 제출 + * @param onboardingData 온보딩 정보 + * @returns 성공 여부 + */ + submitKakaoOnboarding: async (onboardingData: { + gender: "M" | "F"; + height: number; + weight: number; + weightGoal: number; + healthGoal: string; + workoutDaysPerWeek: string; + experienceLevel?: string; + fitnessConcerns?: string; + }): Promise<{ success: boolean; message: string }> => { + try { + const response = await request<{ success: boolean; message: string }>( + "/api/auth/kakao/onboarding", + { + method: "POST", + body: JSON.stringify(onboardingData), + } + ); + return response; + } catch (error: any) { + console.error("[AUTH] 카카오 온보딩 데이터 제출 실패:", error); + throw error; + } + }, }; From 8c6d0fe14e7dc1c91b45be2e7f932b59b83aca4b Mon Sep 17 00:00:00 2001 From: a06246 Date: Sat, 13 Dec 2025 19:47:50 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=EC=B9=B4=EC=B9=B4=EC=98=A4=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=98=A8=EB=B3=B4=EB=94=A9=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=ED=8C=85=20=EB=B6=84=EA=B8=B0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - KakaoOnboardingScreen 생성 (신체정보 수기입력) - authAPI에 submitOnboarding 함수 추가 (/api/users/onboarding) - LoginScreen에서 isOnboarded 값 확인하여 라우팅 분기 처리 - AppNavigator에 KakaoOnboarding 화면 추가 --- src/navigation/AppNavigator.tsx | 2 + src/navigation/types.ts | 1 + src/screens/auth/KakaoOnboardingScreen.tsx | 906 +++++++++++++++++++++ src/screens/auth/LoginScreen.tsx | 29 +- src/services/authAPI.ts | 31 + 5 files changed, 960 insertions(+), 9 deletions(-) create mode 100644 src/screens/auth/KakaoOnboardingScreen.tsx diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index bbc9131..75570ad 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -15,6 +15,7 @@ import LoginScreen from "../screens/auth/LoginScreen"; import SignupScreen from "../screens/auth/SignupScreen"; import FindIdScreen from "../screens/auth/FindIdScreen"; import ResetPasswordScreen from "../screens/auth/ResetPasswordScreen"; +import KakaoOnboardingScreen from "../screens/auth/KakaoOnboardingScreen"; // Main Screens import HomeScreen from "../screens/main/HomeScreen"; @@ -165,6 +166,7 @@ export default function AppNavigator() { name={ROUTES.RESET_PASSWORD} component={ResetPasswordScreen} /> + {/* Main Tabs */} {/* Payment Stack*/} diff --git a/src/navigation/types.ts b/src/navigation/types.ts index af96663..d241805 100644 --- a/src/navigation/types.ts +++ b/src/navigation/types.ts @@ -5,6 +5,7 @@ export type RootStackParamList = { Signup: undefined; FindId: undefined; ResetPassword: undefined; + KakaoOnboarding: undefined; Main: undefined; Chatbot: undefined; Diet: undefined; diff --git a/src/screens/auth/KakaoOnboardingScreen.tsx b/src/screens/auth/KakaoOnboardingScreen.tsx new file mode 100644 index 0000000..854b9a5 --- /dev/null +++ b/src/screens/auth/KakaoOnboardingScreen.tsx @@ -0,0 +1,906 @@ +import React, {useState} from 'react'; +import { + View, + Text, + TextInput, + TouchableOpacity, + StyleSheet, + KeyboardAvoidingView, + Platform, + ScrollView, + Alert, + ActivityIndicator, + Modal, +} from 'react-native'; +import {Picker} from '@react-native-picker/picker'; +import {authAPI} from '../../services'; + +const healthGoalOptions = [ + {label: '벌크업', value: 'BULK'}, + {label: '다이어트', value: 'DIET'}, + {label: '린매스업', value: 'LEAN_MASS'}, + {label: '유지', value: 'MAINTENANCE'}, + {label: '유연성 향상', value: 'FLEXIBILITY'}, + {label: '체력증진', value: 'ENDURANCE'}, + {label: '자세 교정', value: 'POSTURE'}, + {label: '기타', value: 'OTHER'}, +]; + +const workoutDaysOptions = [ + {label: '주 1회', value: '주 1회'}, + {label: '주 2회', value: '주 2회'}, + {label: '주 3회', value: '주 3회'}, + {label: '주 4회', value: '주 4회'}, + {label: '주 5회', value: '주 5회'}, + {label: '주 6회', value: '주 6회'}, + {label: '주 7회', value: '주 7회'}, +]; + +const experienceLevelOptions = [ + {label: '초보자', value: 'BEGINNER'}, + {label: '중급자', value: 'INTERMEDIATE'}, + {label: '고급자', value: 'ADVANCED'}, +]; + +const KakaoOnboardingScreen = ({navigation}: any) => { + const [loading, setLoading] = useState(false); + const [formData, setFormData] = useState({ + birthYear: '', + birthMonth: '', + birthDay: '', + gender: '' as 'M' | 'F' | '', + height: '', + weight: '', + weightGoal: '', + healthGoal: '', + workoutDaysPerWeek: '', + experienceLevel: '', + fitnessConcerns: '', + }); + const [errors, setErrors] = useState({}); + const [pickerModalVisible, setPickerModalVisible] = useState(false); + const [tempPickerValue, setTempPickerValue] = useState({year: '', month: '', day: ''}); + const [genderModalVisible, setGenderModalVisible] = useState(false); + const [workoutDaysModalVisible, setWorkoutDaysModalVisible] = useState(false); + const [healthGoalModalVisible, setHealthGoalModalVisible] = useState(false); + const [experienceLevelModalVisible, setExperienceLevelModalVisible] = useState(false); + + const handleChange = (name: string, value: string) => { + setFormData(prev => ({...prev, [name]: value})); + if (errors[name]) { + setErrors((prev: any) => ({...prev, [name]: ''})); + } + }; + + const generateYearOptions = () => { + const currentYear = new Date().getFullYear(); + const years = []; + for (let i = currentYear; i >= currentYear - 100; i--) { + years.push(i); + } + return years; + }; + + const generateMonthOptions = () => { + return Array.from({length: 12}, (_, i) => i + 1); + }; + + const validateForm = () => { + const newErrors: any = {}; + + if (!formData.birthYear || !formData.birthMonth || !formData.birthDay) { + newErrors.birth = '생년월일을 선택해주세요'; + } + + if (!formData.gender) { + newErrors.gender = '성별을 선택해주세요'; + } + + if (!formData.height.trim()) { + newErrors.height = '키를 입력해주세요'; + } else { + const heightNum = Number(formData.height); + if (heightNum < 100 || heightNum > 250) { + newErrors.height = '키는 100cm 이상 250cm 이하여야 합니다'; + } + } + + if (!formData.weight.trim()) { + newErrors.weight = '체중을 입력해주세요'; + } else { + const weightNum = Number(formData.weight); + if (weightNum < 30 || weightNum > 200) { + newErrors.weight = '체중은 30kg 이상 200kg 이하여야 합니다'; + } + } + + if (!formData.weightGoal.trim()) { + newErrors.weightGoal = '목표 체중을 입력해주세요'; + } else { + const weightGoalNum = Number(formData.weightGoal); + if (weightGoalNum < 30 || weightGoalNum > 200) { + newErrors.weightGoal = '목표 체중은 30kg 이상 200kg 이하여야 합니다'; + } + } + + if (!formData.healthGoal) { + newErrors.healthGoal = '헬스 목적을 선택해주세요'; + } + + if (!formData.workoutDaysPerWeek) { + newErrors.workoutDaysPerWeek = '주간 운동 횟수를 선택해주세요'; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async () => { + if (!validateForm()) { + return; + } + + setLoading(true); + try { + const birthDate = `${formData.birthYear}-${String(formData.birthMonth).padStart(2, '0')}-${String(formData.birthDay).padStart(2, '0')}`; + + const onboardingData = { + gender: formData.gender, + height: Number(formData.height), + weight: Number(formData.weight), + birthDate, + weightGoal: Number(formData.weightGoal), + healthGoal: formData.healthGoal, + workoutDaysPerWeek: formData.workoutDaysPerWeek, + ...(formData.experienceLevel && {experienceLevel: formData.experienceLevel}), + ...(formData.fitnessConcerns && {fitnessConcerns: formData.fitnessConcerns}), + }; + + const response = await authAPI.submitOnboarding(onboardingData); + + if (response.success) { + Alert.alert('완료', '신체정보가 저장되었습니다.', [ + { + text: '확인', + onPress: () => navigation.replace('Main'), + }, + ]); + } else { + Alert.alert('오류', response.message || '신체정보 저장에 실패했습니다.'); + } + } catch (error: any) { + console.error('온보딩 제출 실패:', error); + Alert.alert('오류', error.message || '신체정보 저장에 실패했습니다.'); + } finally { + setLoading(false); + } + }; + + return ( + + + + 신체정보를 입력해주세요 + 맞춤형 서비스를 제공하기 위해 필요합니다 + + + + {/* 생년월일 */} + + { + const currentYear = new Date().getFullYear(); + const defaultYear = formData.birthYear || String(currentYear - 20); + const defaultMonth = formData.birthMonth || '1'; + const defaultDay = formData.birthDay || '1'; + + setTempPickerValue({ + year: defaultYear, + month: defaultMonth, + day: defaultDay, + }); + setPickerModalVisible(true); + }}> + + + {errors.birth && ( + {errors.birth} + )} + + + {/* 생년월일 선택 모달 */} + setPickerModalVisible(false)}> + setPickerModalVisible(false)}> + {}} + style={styles.modalContent}> + + setPickerModalVisible(false)}> + 취소 + + 생년월일 선택 + { + handleChange('birthYear', tempPickerValue.year); + handleChange('birthMonth', tempPickerValue.month); + handleChange('birthDay', tempPickerValue.day); + setPickerModalVisible(false); + }}> + 확인 + + + + + + { + const newValue = {...tempPickerValue, year: value}; + if (newValue.year && newValue.month) { + const daysInMonth = new Date( + Number(newValue.year), + Number(newValue.month), + 0 + ).getDate(); + const currentDay = Number(newValue.day) || 1; + if (currentDay > daysInMonth) { + newValue.day = '1'; + } + } + setTempPickerValue(newValue); + }} + style={styles.modalPicker} + itemStyle={styles.pickerItemStyle}> + {generateYearOptions().map(year => ( + + ))} + + + + + { + const newValue = {...tempPickerValue, month: value}; + if (newValue.year && newValue.month) { + const daysInMonth = new Date( + Number(newValue.year), + Number(newValue.month), + 0 + ).getDate(); + const currentDay = Number(newValue.day) || 1; + if (currentDay > daysInMonth) { + newValue.day = '1'; + } + } + setTempPickerValue(newValue); + }} + style={styles.modalPicker} + itemStyle={styles.pickerItemStyle}> + {generateMonthOptions().map(month => ( + + ))} + + + + + { + setTempPickerValue({...tempPickerValue, day: value}); + }} + style={styles.modalPicker} + itemStyle={styles.pickerItemStyle}> + {(() => { + if (tempPickerValue.year && tempPickerValue.month) { + const year = Number(tempPickerValue.year); + const month = Number(tempPickerValue.month); + const daysInMonth = new Date(year, month, 0).getDate(); + const days = Array.from({length: daysInMonth}, (_, i) => i + 1); + + const currentDay = Number(tempPickerValue.day) || 1; + if (currentDay > daysInMonth) { + setTimeout(() => { + setTempPickerValue({...tempPickerValue, day: '1'}); + }, 0); + } + + return days; + } + return Array.from({length: 31}, (_, i) => i + 1); + })().map(day => ( + + ))} + + + + + + + + {/* 성별 */} + + setGenderModalVisible(true)}> + + + {errors.gender && ( + {errors.gender} + )} + + + {/* 성별 선택 모달 */} + setGenderModalVisible(false)}> + setGenderModalVisible(false)}> + {}} + style={styles.modalContent}> + + setGenderModalVisible(false)}> + 취소 + + 성별 선택 + + + + { + handleChange('gender', 'M'); + setGenderModalVisible(false); + }}> + + 남성 + + + { + handleChange('gender', 'F'); + setGenderModalVisible(false); + }}> + + 여성 + + + + + + + + {/* 키, 체중 */} + + + handleChange('height', text)} + keyboardType="number-pad" + placeholderTextColor="rgba(255, 255, 255, 0.7)" + /> + {errors.height && ( + {errors.height} + )} + + + + handleChange('weight', text)} + keyboardType="number-pad" + placeholderTextColor="rgba(255, 255, 255, 0.7)" + /> + {errors.weight && ( + {errors.weight} + )} + + + + {/* 목표 체중 */} + + handleChange('weightGoal', text)} + keyboardType="number-pad" + placeholderTextColor="rgba(255, 255, 255, 0.7)" + /> + {errors.weightGoal && ( + {errors.weightGoal} + )} + + + {/* 헬스 목적 */} + + setHealthGoalModalVisible(true)}> + opt.value === formData.healthGoal)?.label || '' + : '' + } + placeholder="헬스 목적" + placeholderTextColor="rgba(255, 255, 255, 0.7)" + editable={false} + pointerEvents="none" + /> + + {errors.healthGoal && ( + {errors.healthGoal} + )} + + + {/* 헬스 목적 선택 모달 */} + setHealthGoalModalVisible(false)}> + setHealthGoalModalVisible(false)}> + {}} + style={styles.modalContent}> + + setHealthGoalModalVisible(false)}> + 취소 + + 헬스 목적 선택 + + + + + {healthGoalOptions.map((option) => ( + { + handleChange('healthGoal', option.value); + setHealthGoalModalVisible(false); + }}> + + {option.label} + + + ))} + + + + + + + {/* 주간 운동 횟수 */} + + setWorkoutDaysModalVisible(true)}> + opt.value === formData.workoutDaysPerWeek)?.label || '' + : '' + } + placeholder="주간 운동 횟수" + placeholderTextColor="rgba(255, 255, 255, 0.7)" + editable={false} + pointerEvents="none" + /> + + {errors.workoutDaysPerWeek && ( + {errors.workoutDaysPerWeek} + )} + + + {/* 주간 운동 횟수 선택 모달 */} + setWorkoutDaysModalVisible(false)}> + setWorkoutDaysModalVisible(false)}> + {}} + style={styles.modalContent}> + + setWorkoutDaysModalVisible(false)}> + 취소 + + 주간 운동 횟수 선택 + + + + + {workoutDaysOptions.map((option) => ( + { + handleChange('workoutDaysPerWeek', option.value); + setWorkoutDaysModalVisible(false); + }}> + + {option.label} + + + ))} + + + + + + + {/* 운동 경험 수준 (선택) */} + + setExperienceLevelModalVisible(true)}> + opt.value === formData.experienceLevel)?.label || '' + : '' + } + placeholder="운동 경험 수준 (선택)" + placeholderTextColor="rgba(255, 255, 255, 0.7)" + editable={false} + pointerEvents="none" + /> + + + + {/* 운동 경험 수준 선택 모달 */} + setExperienceLevelModalVisible(false)}> + setExperienceLevelModalVisible(false)}> + {}} + style={styles.modalContent}> + + setExperienceLevelModalVisible(false)}> + 취소 + + 운동 경험 수준 선택 + + + + + {experienceLevelOptions.map((option) => ( + { + handleChange('experienceLevel', option.value); + setExperienceLevelModalVisible(false); + }}> + + {option.label} + + + ))} + + + + + + + {/* 헬스 고민 (선택) */} + + handleChange('fitnessConcerns', text)} + placeholderTextColor="rgba(255, 255, 255, 0.7)" + multiline + /> + + + + + {loading ? ( + + ) : ( + 완료 + )} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#252525', + }, + scrollContent: { + flexGrow: 1, + paddingHorizontal: 20, + paddingTop: 60, + paddingBottom: 40, + }, + header: { + marginBottom: 30, + }, + title: { + fontSize: 24, + fontWeight: '700', + color: '#ffffff', + marginBottom: 8, + }, + subtitle: { + fontSize: 14, + color: 'rgba(255, 255, 255, 0.7)', + }, + form: { + marginBottom: 30, + }, + inputGroup: { + marginBottom: 20, + }, + inputRow: { + flexDirection: 'row', + gap: 12, + }, + inputHalf: { + flex: 1, + }, + input: { + width: '100%', + height: 60, + backgroundColor: '#434343', + borderWidth: 0, + borderRadius: 20, + paddingHorizontal: 20, + fontSize: 16, + fontWeight: '400', + color: '#ffffff', + }, + birthDateButtonContainer: { + width: '100%', + }, + errorMessage: { + color: '#ff6b6b', + fontSize: 14, + marginTop: 5, + marginLeft: 5, + }, + submitBtn: { + width: '100%', + height: 60, + backgroundColor: '#ffffff', + borderRadius: 20, + alignItems: 'center', + justifyContent: 'center', + }, + submitBtnDisabled: { + opacity: 0.6, + }, + submitBtnText: { + color: '#000000', + fontSize: 18, + fontWeight: '700', + }, + modalOverlay: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + justifyContent: 'flex-end', + }, + modalContent: { + backgroundColor: '#252525', + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + paddingBottom: 40, + }, + modalHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 20, + paddingVertical: 16, + borderBottomWidth: 1, + borderBottomColor: '#434343', + }, + modalTitle: { + fontSize: 18, + fontWeight: '700', + color: '#ffffff', + }, + modalCancelText: { + fontSize: 16, + color: 'rgba(255, 255, 255, 0.7)', + }, + modalConfirmText: { + fontSize: 16, + color: '#ffffff', + fontWeight: '600', + }, + birthPickerGroup: { + flexDirection: 'row', + paddingHorizontal: 20, + paddingVertical: 20, + }, + birthPickerItem: { + flex: 1, + alignItems: 'center', + }, + birthPickerLabelTop: { + fontSize: 14, + color: 'rgba(255, 255, 255, 0.7)', + marginBottom: 8, + }, + modalPicker: { + width: '100%', + height: 150, + }, + pickerItemStyle: { + color: '#ffffff', + }, + genderOptionContainer: { + paddingHorizontal: 20, + paddingVertical: 20, + }, + genderOption: { + paddingVertical: 16, + paddingHorizontal: 20, + backgroundColor: '#434343', + borderRadius: 12, + marginBottom: 12, + alignItems: 'center', + }, + genderOptionSelected: { + backgroundColor: '#ffffff', + }, + genderOptionText: { + fontSize: 16, + color: '#ffffff', + }, + genderOptionTextSelected: { + color: '#000000', + fontWeight: '600', + }, + modalOptionContainer: { + paddingHorizontal: 20, + paddingVertical: 20, + }, + optionGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 12, + }, + optionButton: { + flex: 1, + minWidth: '45%', + paddingVertical: 16, + paddingHorizontal: 20, + backgroundColor: '#434343', + borderRadius: 12, + alignItems: 'center', + }, + optionButtonSelected: { + backgroundColor: '#ffffff', + }, + optionButtonText: { + fontSize: 16, + color: '#ffffff', + }, + optionButtonTextSelected: { + color: '#000000', + fontWeight: '600', + }, +}); + +export default KakaoOnboardingScreen; + diff --git a/src/screens/auth/LoginScreen.tsx b/src/screens/auth/LoginScreen.tsx index ee3290d..f6a4f77 100644 --- a/src/screens/auth/LoginScreen.tsx +++ b/src/screens/auth/LoginScreen.tsx @@ -19,6 +19,7 @@ import {authAPI} from '../../services'; import {ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY} from '../../services/apiConfig'; // @react-native-seoul/kakao-login (네이티브 빌드에서만 작동) +// Expo Go에서는 WebBrowser.openAuthSessionAsync 사용 let KakaoLogin: any = null; try { KakaoLogin = require('@react-native-seoul/kakao-login'); @@ -125,9 +126,17 @@ const LoginScreen = ({navigation}: any) => { const membershipType = (parsed.queryParams?.membershipType as string | undefined) || 'FREE'; await AsyncStorage.setItem('membershipType', membershipType); - console.log('✅ [카카오 로그인] 토큰 저장 완료, 메인 화면으로 이동'); - // 메인 화면으로 이동 - navigation.replace('Main'); + // 온보딩 여부 확인 + const isOnboarded = parsed.queryParams?.isOnboarded; + const onboarded = parsed.queryParams?.onboarded; + const shouldOnboard = isOnboarded === 'false' || onboarded === 'false'; + + console.log('✅ [카카오 로그인] 토큰 저장 완료'); + if (shouldOnboard) { + navigation.replace('KakaoOnboarding'); + } else { + navigation.replace('Main'); + } } catch (error: any) { console.error('❌ [카카오 로그인] 토큰 저장 실패:', error); Alert.alert('로그인 실패', error.message || '토큰 저장 중 오류가 발생했습니다.'); @@ -181,9 +190,10 @@ const LoginScreen = ({navigation}: any) => { await AsyncStorage.setItem('membershipType', 'FREE'); } - // 온보딩 여부 확인 - if (data.onboarded === false) { - navigation.replace('Onboarding'); + // 온보딩 여부 확인 (isOnboarded 또는 onboarded 값 확인) + const isOnboarded = data.isOnboarded !== undefined ? data.isOnboarded : data.onboarded; + if (isOnboarded === false) { + navigation.replace('KakaoOnboarding'); } else { navigation.replace('Main'); } @@ -320,9 +330,10 @@ const LoginScreen = ({navigation}: any) => { await AsyncStorage.setItem('membershipType', 'FREE'); } - // 온보딩 여부 확인 - if (data.onboarded === false) { - navigation.replace('Onboarding'); + // 온보딩 여부 확인 (isOnboarded 또는 onboarded 값 확인) + const isOnboarded = data.isOnboarded !== undefined ? data.isOnboarded : data.onboarded; + if (isOnboarded === false) { + navigation.replace('KakaoOnboarding'); } else { navigation.replace('Main'); } diff --git a/src/services/authAPI.ts b/src/services/authAPI.ts index 653fb86..08a8994 100644 --- a/src/services/authAPI.ts +++ b/src/services/authAPI.ts @@ -838,4 +838,35 @@ export const authAPI = { throw error; } }, + + /** + * 온보딩 데이터 제출 (신체정보 수기입력) + * @param onboardingData 온보딩 정보 + * @returns 성공 여부 + */ + submitOnboarding: async (onboardingData: { + gender: "M" | "F"; + height: number; + weight: number; + birthDate: string; + weightGoal: number; + healthGoal: string; + workoutDaysPerWeek: string; + experienceLevel?: string; + fitnessConcerns?: string; + }): Promise<{ success: boolean; message: string }> => { + try { + const response = await request<{ success: boolean; message: string }>( + "/api/users/onboarding", + { + method: "POST", + body: JSON.stringify(onboardingData), + } + ); + return response; + } catch (error: any) { + console.error("[AUTH] 온보딩 데이터 제출 실패:", error); + throw error; + } + }, };