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 358b2c0..969c4b0 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 d6c015e..08a8994 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,105 @@ 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;
+ }
+ },
+
+ /**
+ * 온보딩 데이터 제출 (신체정보 수기입력)
+ * @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;
+ }
+ },
};