diff --git a/app/FunnelScreen.jsx b/app/FunnelScreen.jsx deleted file mode 100644 index f484ccd..0000000 --- a/app/FunnelScreen.jsx +++ /dev/null @@ -1,269 +0,0 @@ -/* eslint-disable react/no-unstable-nested-components */ -import React, { useContext } from 'react'; -import { StyleSheet, Platform, Linking } from 'react-native'; -import { useFunnel } from '../hooks/funnel/useFunnel'; -import { - Layout, - Text, - Select, - SelectItem, - Button, - TopNavigation, - TopNavigationAction, - Icon, -} from '@ui-kitten/components'; -import * as Animatable from 'react-native-animatable'; -import messaging from '@react-native-firebase/messaging'; -import { useRouter } from 'expo-router'; -import { FunnelContext } from '@/contexts/FunnelContext'; -import useModal from '../hooks/common/useModal'; -import ConfirmModal from '../components/common/molecules/ConfirmModal'; - -const FunnelScreen = () => { - const { Funnel, setStep, goBack, currentStep } = useFunnel('Step1'); - const route = useRouter(); - const { setFunnelDone } = useContext(FunnelContext); - const { isVisible, setIsVisible } = useModal(); - - // 직업과 나이 옵션 데이터 - const jobOptions = ['개발자', '디자이너', '기획자', '마케터', '기타']; - const ageOptions = ['10대', '20대', '30대', '40대', '50대 이상']; - - // 상태 관리 - const [selectedJobIndex, setSelectedJobIndex] = React.useState(null); - const [selectedAgeIndex, setSelectedAgeIndex] = React.useState(null); - - // 알림 권한 요청 함수 - const requestNotificationPermission = async () => { - try { - const authStatus = await messaging().hasPermission(); - - const enabled = - authStatus === messaging.AuthorizationStatus.AUTHORIZED || - authStatus === messaging.AuthorizationStatus.PROVISIONAL; - - if (enabled) { - // 권한이 이미 허용됨 - return true; - } else { - if (Platform.OS === 'ios') { - // iOS에서 권한 요청 - const authStatusOnIOS = await messaging().requestPermission(); - const enabledOnIOS = - authStatusOnIOS === messaging.AuthorizationStatus.AUTHORIZED || - authStatusOnIOS === messaging.AuthorizationStatus.PROVISIONAL; - return enabledOnIOS; - } else if (Platform.OS === 'android') { - // Android에서 버전 확인 - const androidVersion = Platform.Version; - if (androidVersion >= 33) { - // Android 13 이상에서 권한 요청 - const result = await messaging().requestPermission(); - const enabledOnAndroid = - result === messaging.AuthorizationStatus.AUTHORIZED; - return enabledOnAndroid; - } else { - // Android 13 미만은 권한 요청 불필요 - return true; - } - } else { - // 기타 플랫폼 (web 등) - return false; - } - } - } catch (error) { - console.error('Notification permission error:', error); - return false; - } - }; - - // 애니메이션 Ref - const ageSelectRef = React.useRef(null); - const nextButtonRef = React.useRef(null); - - // 직업이 선택되었을 때 나이 드롭다운 애니메이션 실행 - React.useEffect(() => { - if (selectedJobIndex !== null && ageSelectRef.current) { - ageSelectRef.current.fadeInUp(500); - } - }, [selectedJobIndex]); - - // 나이가 선택되었을 때 다음 버튼 애니메이션 실행 - React.useEffect(() => { - if (selectedAgeIndex !== null && nextButtonRef.current) { - nextButtonRef.current.fadeInUp(500); - } - }, [selectedAgeIndex]); - - // Back Icon Component - const BackIcon = props => ; - - // Back Action Component - const BackAction = () => ( - - ); - - // Handle Back Button Press - const handleBackAction = () => { - if (currentStep === 'Step1') { - route.back(); - } else { - goBack(); - } - }; - - return ( - - {/* 커스텀 헤더 */} - - - {/* 펀넬 내용 */} - - {/* Step 1 */} - - - - 직업과 나이를 선택해주세요 - - - {/* 직업 드롭다운 */} - - - {/* 직업이 선택되면 나이 드롭다운 표시 */} - {selectedJobIndex !== null && ( - - - - )} - - {/* 나이가 선택되면 다음 버튼 표시 */} - {selectedAgeIndex !== null && ( - - - - )} - - - - {/* Step 2 */} - - - - 알림을 허용하시겠습니까? - - - - { - setIsVisible(false); - setStep('Step3'); - }} - onCancel={() => { - setIsVisible(false); - Linking.openSettings(); - }} - titleKey="" - messageKey="" - confirmTextKey="" - cancelTextKey="" - /> - - - {/* Step 3 */} - - - - 설정이 완료되었습니다! - - - - - - - ); -}; - -export default FunnelScreen; - -const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - padding: 20, - backgroundColor: '#FFFFFF', - }, - title: { - textAlign: 'center', - marginBottom: 30, - }, - select: { - marginVertical: 10, - }, - button: { - marginTop: 30, - }, - animatableView: { - width: '100%', - }, -}); diff --git a/app/_layout.jsx b/app/_layout.jsx index 96dcb22..25f339e 100755 --- a/app/_layout.jsx +++ b/app/_layout.jsx @@ -16,6 +16,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { default as mapping } from '../theme/mapping.json'; +import FunnelProvider from '@/contexts/FunnelContext'; const SENTRY_MODE = env.SENTRY_MODE; Sentry.init({ @@ -53,73 +54,81 @@ const RootLayout = () => { theme={{ ...eva.light, ...theme }} customMapping={mapping} > - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/app/funnelView/funnelView.jsx b/app/funnelView/funnelView.jsx new file mode 100644 index 0000000..6bf35ca --- /dev/null +++ b/app/funnelView/funnelView.jsx @@ -0,0 +1,115 @@ +/* eslint-disable react/no-unstable-nested-components */ +import React, { useState } from 'react'; +import { useFunnel } from '../../hooks/funnel/useFunnel'; +import { + Layout, + TopNavigation, + TopNavigationAction, + Icon, +} from '@ui-kitten/components'; +import { useRouter } from 'expo-router'; +import NameInputScreen from './stepOneView'; +import AgeSelectionScreen from './stepTwoView'; +import SleepTimeScreen from './stepFourView'; +import DelayReasonsScreen from './stepFiveView'; +import CompletionScreen from './stepLastView'; +import JobSelectionScreen from './stepThreeView'; + +const FunnelScreen = () => { + const { Funnel, setStep, goBack, currentStep } = useFunnel('Step1'); + const route = useRouter(); + const [name, setName] = useState(''); + const [selectedAge, setSelectedAge] = useState(null); + const [selectedTime, setSelectedTime] = useState(null); + const [selectedPeriod, setSelectedPeriod] = useState(null); + const [selectedReasons, setSelectedReasons] = useState([]); + const [selectedJob, setSelectedJob] = useState(null); + + const BackIcon = props => ; + + const BackAction = () => ( + + ); + + const handleBackAction = () => { + if (currentStep === 'Step1') { + route.back(); + } else { + goBack(); + } + }; + + return ( + + + + + + { + setStep('Step2'); + }} + name={name} + setName={setName} + /> + + + { + setStep('Step3'); + }} + selectedAge={selectedAge} + setSelectedAge={setSelectedAge} + /> + + + setStep('Step4')} + selectedJob={selectedJob} + setSelectedJob={setSelectedJob} + /> + + + { + setStep('Step5'); + }} + selectedTime={selectedTime} + setSelectedTime={setSelectedTime} + selectedPeriod={selectedPeriod} + setSelectedPeriod={setSelectedPeriod} + /> + + + { + setStep('StepLast'); + }} + selectedReasons={selectedReasons} + setSelectedReasons={setSelectedReasons} + /> + + + + + + + ); +}; + +export default FunnelScreen; diff --git a/app/funnelView/stepFiveView.tsx b/app/funnelView/stepFiveView.tsx new file mode 100644 index 0000000..8a5145e --- /dev/null +++ b/app/funnelView/stepFiveView.tsx @@ -0,0 +1,180 @@ +import React from 'react'; +import { + Layout, + Text, + Button, + StyleService, + useStyleSheet, +} from '@ui-kitten/components'; +import { View, TouchableOpacity } from 'react-native'; +import { scale, verticalScale, moderateScale } from 'react-native-size-matters'; +import * as Animatable from 'react-native-animatable'; + +const DelayReasonsScreen = ({ + onNext, + currentStep = 4, + totalSteps = 5, + selectedReasons, + setSelectedReasons, +}) => { + const styles = useStyleSheet(themedStyles); + const [localSelectedReasons, setLocalSelectedReasons] = + React.useState(selectedReasons); + + const reasons = [ + '할 일들이 너무 크게 느껴져요', + '꾸준히 이어가기 어려워요', + '우선순위를 정하기 어려워요', + '동기 부여가 없어요', + '집중력이 부족해요', + '심리적으로 불안해요', + ]; + + const toggleReason = index => { + setLocalSelectedReasons(prev => { + const isSelected = prev.includes(index); + if (isSelected) { + return prev.filter(i => i !== index); + } else if (prev.length < 3) { + return [...prev, index]; + } + return prev; + }); + }; + + const renderStepIndicators = () => ( + + {[...Array(totalSteps)].map((_, index) => ( + + ))} + + ); + + return ( + + {renderStepIndicators()} + + 미루는 이유를 알려주세요 + 최대 3개까지 선택할 수 있어요 + + + {reasons.map((reason, index) => ( + toggleReason(index)} + > + + {reason} + + + ))} + + {localSelectedReasons.length > 0 && ( + + + + )} + + + ); +}; + +const themedStyles = StyleService.create({ + container: { + flex: 1, + backgroundColor: 'background-basic-color-1', + paddingHorizontal: scale(20), + }, + contentContainer: { + flex: 1, + justifyContent: 'space-between', + paddingBottom: verticalScale(20), + }, + stepContainer: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + marginBottom: verticalScale(40), + }, + stepIndicator: { + width: scale(8), + height: scale(8), + borderRadius: scale(4), + backgroundColor: 'color-basic-400', + marginHorizontal: scale(3), + }, + activeStepIndicator: { + backgroundColor: 'color-primary-500', + width: scale(16), + height: scale(8), + }, + title: { + textAlign: 'left', + fontSize: moderateScale(28), + fontWeight: 'bold', + marginBottom: verticalScale(8), + }, + subtitle: { + textAlign: 'left', + fontSize: moderateScale(16), + color: 'text-hint-color', + marginBottom: verticalScale(30), + }, + reasonsContainer: { + gap: verticalScale(10), + }, + reasonOption: { + padding: moderateScale(16), + borderRadius: moderateScale(8), + backgroundColor: 'background-basic-color-2', + borderWidth: 1, + borderColor: 'transparent', + }, + selectedReason: { + backgroundColor: 'color-primary-100', + borderColor: 'color-primary-500', + }, + reasonText: { + fontSize: moderateScale(16), + color: 'text-basic-color', + }, + selectedReasonText: { + color: 'color-primary-700', + fontWeight: '600', + }, + button: { + borderRadius: moderateScale(8), + }, +}); + +export default DelayReasonsScreen; diff --git a/app/funnelView/stepFourView.tsx b/app/funnelView/stepFourView.tsx new file mode 100644 index 0000000..a489cc8 --- /dev/null +++ b/app/funnelView/stepFourView.tsx @@ -0,0 +1,335 @@ +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import { + Layout, + Text, + Button, + StyleService, + useStyleSheet, +} from '@ui-kitten/components'; +import { View, TouchableOpacity, Pressable } from 'react-native'; +import { scale, verticalScale, moderateScale } from 'react-native-size-matters'; +import BottomSheet, { + BottomSheetView, + BottomSheetBackdrop, +} from '@gorhom/bottom-sheet'; +import * as Animatable from 'react-native-animatable'; + +const SleepTimeScreen = ({ + onNext, + currentStep = 3, + totalSteps = 5, + selectedTime, + setSelectedTime, + selectedPeriod, + setSelectedPeriod, +}) => { + const [localSelectedTime, setLocalSelectedTime] = useState(selectedTime); + const [localSelectedPeriod, setLocalSelectedPeriod] = + useState(selectedPeriod); + const [currentSheet, setCurrentSheet] = useState(null); + const [showTimeSelector, setShowTimeSelector] = useState( + localSelectedTime && localSelectedPeriod, + ); + const styles = useStyleSheet(themedStyles); + + // refs + const timeSheetRef = useRef(null); + const periodSheetRef = useRef(null); + + // variables + const snapPoints = useMemo(() => ['50%'], []); + const timeOptions = Array.from({ length: 12 }, (_, i) => `${i + 1}시`); + const periodOptions = ['오전', '오후']; + + // callbacks + const handleSheetChanges = useCallback(index => { + if (index === -1) { + setCurrentSheet(null); + } + }, []); + + const handleClosePress = useCallback(() => { + if (currentSheet === 'time') { + timeSheetRef.current?.close(); + } else if (currentSheet === 'period') { + periodSheetRef.current?.close(); + } + setCurrentSheet(null); + }, [currentSheet]); + + const handlePeriodSelect = period => { + setLocalSelectedPeriod(period); + periodSheetRef.current?.close(); + setShowTimeSelector(true); + }; + + const handleTimeSelect = time => { + setLocalSelectedTime(time); + timeSheetRef.current?.close(); + }; + + const renderBackdrop = useCallback( + props => ( + + ), + [], + ); + + const renderStepIndicators = () => ( + + {[...Array(totalSteps)].map((_, index) => ( + + ))} + + ); + + return ( + + {renderStepIndicators()} + + 평소 몇시에 주무시나요? + + + { + setCurrentSheet('period'); + periodSheetRef.current?.expand(); + }} + > + + {localSelectedPeriod || '시간대'} + + + + + {showTimeSelector && ( + + { + setCurrentSheet('time'); + timeSheetRef.current?.expand(); + }} + > + + {localSelectedTime || '시간'} + + + + + )} + + + {localSelectedTime && localSelectedPeriod && ( + + + + )} + + + {/* Time Bottom Sheet */} + + + + 시간 선택 + + × + + + + {timeOptions.map((time, index) => ( + handleTimeSelect(time)} + > + {time} + + ))} + + + + + {/* Period Bottom Sheet */} + + + + 시간대 선택 + + × + + + + {periodOptions.map((period, index) => ( + handlePeriodSelect(period)} + > + {period} + + ))} + + + + + ); +}; + +const themedStyles = StyleService.create({ + container: { + flex: 1, + backgroundColor: 'background-basic-color-1', + paddingHorizontal: scale(20), + }, + stepContainer: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + marginBottom: verticalScale(40), + }, + contentContainer: { + flex: 1, + justifyContent: 'space-between', + paddingBottom: verticalScale(20), // 하단 간격 + }, + stepIndicator: { + width: scale(8), + height: scale(8), + borderRadius: scale(4), + backgroundColor: 'color-basic-400', + marginHorizontal: scale(3), + }, + activeStepIndicator: { + backgroundColor: 'color-primary-500', + width: scale(16), + height: scale(8), + }, + title: { + textAlign: 'left', + marginBottom: verticalScale(30), + fontSize: moderateScale(28), + fontWeight: 'bold', + }, + selector: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: moderateScale(15), + borderRadius: moderateScale(8), + backgroundColor: 'background-basic-color-1', + borderWidth: 1, + borderColor: 'color-basic-400', + marginBottom: verticalScale(10), + }, + placeholderText: { + color: 'text-hint-color', + }, + selectedText: { + color: 'text-basic-color', + }, + arrow: { + color: 'color-basic-600', + }, + bottomSheetContainer: { + flex: 1, + backgroundColor: 'background-basic-color-1', + }, + bottomSheetHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: moderateScale(15), + borderBottomWidth: 1, + borderBottomColor: 'color-basic-300', + }, + bottomSheetTitle: { + fontSize: moderateScale(16), + fontWeight: 'bold', + }, + closeButton: { + fontSize: moderateScale(24), + color: 'color-basic-600', + padding: moderateScale(5), + }, + optionsContainer: { + padding: moderateScale(15), + }, + optionItem: { + padding: moderateScale(15), + }, + optionText: { + textAlign: 'center', + fontSize: moderateScale(16), + }, + buttonContainer: {}, + button: { + borderRadius: moderateScale(8), + }, + timeGridContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + padding: moderateScale(15), + }, + timeGridItem: { + width: '33.33%', + padding: moderateScale(10), + alignItems: 'center', + justifyContent: 'center', + }, + selectorContainer: {}, +}); + +export default SleepTimeScreen; diff --git a/app/funnelView/stepLastView.tsx b/app/funnelView/stepLastView.tsx new file mode 100644 index 0000000..34fe839 --- /dev/null +++ b/app/funnelView/stepLastView.tsx @@ -0,0 +1,202 @@ +import React, { useEffect, useState } from 'react'; +import { + Layout, + Text, + Button, + StyleService, + useStyleSheet, +} from '@ui-kitten/components'; +import { View, Platform, Linking, TouchableOpacity } from 'react-native'; +import * as Animatable from 'react-native-animatable'; +import { scale, verticalScale, moderateScale } from 'react-native-size-matters'; +import { router } from 'expo-router'; +import messaging from '@react-native-firebase/messaging'; +import ConfirmModal from '@/components/common/molecules/ConfirmModal'; +import useModal from '@/hooks/common/useModal'; +import { useStorage } from '@/hooks/auth/useStorage'; + +const requestNotificationPermission = async () => { + try { + const authStatus = await messaging().hasPermission(); + + const enabled = + authStatus === messaging.AuthorizationStatus.AUTHORIZED || + authStatus === messaging.AuthorizationStatus.PROVISIONAL; + + if (enabled) { + // 권한이 이미 허용됨 + return true; + } else { + if (Platform.OS === 'ios') { + // iOS에서 권한 요청 + const authStatusOnIOS = await messaging().requestPermission(); + const enabledOnIOS = + authStatusOnIOS === messaging.AuthorizationStatus.AUTHORIZED || + authStatusOnIOS === messaging.AuthorizationStatus.PROVISIONAL; + return enabledOnIOS; + } else if (Platform.OS === 'android') { + // Android에서 버전 확인 + const androidVersion = Platform.Version; + if (androidVersion >= 33) { + // Android 13 이상에서 권한 요청 + const result = await messaging().requestPermission(); + const enabledOnAndroid = + result === messaging.AuthorizationStatus.AUTHORIZED; + return enabledOnAndroid; + } else { + // Android 13 미만은 권한 요청 불필요 + return true; + } + } else { + // 기타 플랫폼 (web 등) + return false; + } + } + } catch (error) { + console.error('Notification permission error:', error); + return false; + } +}; + +const CompletionScreen = ({ name, userInfo }) => { + const [showNotificationScreen, setShowNotificationScreen] = useState(false); + const styles = useStyleSheet(themedStyles); + const { isVisible, setIsVisible } = useModal(); + const { setItem } = useStorage(); + + useEffect(() => { + // 3초 후에 알림 허용 화면으로 전환 + const timer = setTimeout(() => { + setShowNotificationScreen(true); + }, 3000); + + return () => clearTimeout(timer); + }, []); + + const handleAllowNotifications = async () => { + const granted = await requestNotificationPermission(); + if (granted) { + router.dismissAll(); + router.replace('/(tabs)'); + } else { + setIsVisible(true); + } + }; + + if (!showNotificationScreen) { + return ( + + + 환영합니다 {name}님 + 가입이 완료되었어요 + + + ); + } + + return ( + + + + 할 일을 잊지 않도록{'\n'}알려드릴게요 + + + { + setIsVisible(true); + setItem('userInfo', JSON.stringify(userInfo)); + }} + > + 지금은 괜찮아요 + + + + + + { + setIsVisible(false); + Linking.openSettings(); + router.dismissAll(); + router.replace('/(tabs)'); + }} + onCancel={() => { + setIsVisible(false); + router.dismissAll(); + router.replace('/(tabs)'); + }} + titleKey="views.index.AlertModalTitle" + messageKey="views.index.AlertModalMessage" + confirmTextKey="common.yes" + cancelTextKey="common.no" + /> + + + ); +}; + +const themedStyles = StyleService.create({ + container: { + flex: 1, + backgroundColor: 'background-basic-color-1', + paddingHorizontal: scale(20), + }, + completionContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + welcomeText: { + fontSize: moderateScale(18), + marginBottom: verticalScale(8), + color: 'text-basic-color', + }, + completionText: { + fontSize: moderateScale(24), + fontWeight: 'bold', + color: 'text-basic-color', + }, + notificationContainer: { + flex: 1, + justifyContent: 'space-between', + paddingTop: verticalScale(88), + paddingBottom: verticalScale(30), + }, + notificationTitle: { + fontSize: moderateScale(28), + fontWeight: 'bold', + textAlign: 'left', + lineHeight: moderateScale(35), + }, + buttonContainer: { + gap: verticalScale(16), + }, + skipText: { + textAlign: 'center', + color: 'text-hint-color', + fontSize: moderateScale(14), + }, + button: { + borderRadius: moderateScale(8), + }, +}); + +export default CompletionScreen; diff --git a/app/funnelView/stepOneView.tsx b/app/funnelView/stepOneView.tsx new file mode 100644 index 0000000..186698d --- /dev/null +++ b/app/funnelView/stepOneView.tsx @@ -0,0 +1,147 @@ +import React from 'react'; +import { + Layout, + Input, + Button, + Text, + StyleService, + useStyleSheet, +} from '@ui-kitten/components'; +import * as Animatable from 'react-native-animatable'; +import { View, KeyboardAvoidingView, Platform } from 'react-native'; +import { scale, verticalScale, moderateScale } from 'react-native-size-matters'; + +const NameInputScreen = ({ + onNext, + currentStep = 0, + totalSteps = 5, + name, + setName, +}) => { + const styles = useStyleSheet(themedStyles); + const [inputContent, setInputContent] = React.useState(name); + + const renderStepIndicators = () => { + return ( + + {[...Array(totalSteps)].map((_, index) => ( + + ))} + + ); + }; + + return ( + + + {renderStepIndicators()} + + 이름을 입력해주세요 + + + + + + + {inputContent !== '' && ( + + + + )} + + + + ); +}; + +const themedStyles = StyleService.create({ + keyboardAvoidingView: { + flex: 1, + }, + container: { + flex: 1, + backgroundColor: 'background-basic-color-1', + paddingHorizontal: scale(20), + }, + contentContainer: { + flex: 1, + justifyContent: 'space-between', + paddingBottom: verticalScale(20), // 하단 간격 + }, + stepContainer: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + marginBottom: verticalScale(40), + }, + stepIndicator: { + width: scale(8), + height: scale(8), + borderRadius: scale(4), + backgroundColor: 'color-basic-400', + marginHorizontal: scale(3), + }, + activeStepIndicator: { + backgroundColor: 'color-primary-500', + width: scale(16), + height: scale(8), + }, + title: { + textAlign: 'left', + marginBottom: verticalScale(30), + fontSize: moderateScale(28), + fontWeight: 'bold', + color: 'text-basic-color', + }, + inputContainer: {}, + input: { + borderRadius: moderateScale(8), + backgroundColor: 'background-basic-color-1', + borderColor: 'color-basic-400', + }, + inputText: { + fontSize: moderateScale(16), + }, + buttonContainer: {}, + button: { + borderRadius: moderateScale(8), + backgroundColor: 'color-primary-default', + }, +}); + +export default NameInputScreen; diff --git a/app/funnelView/stepThreeView.tsx b/app/funnelView/stepThreeView.tsx new file mode 100644 index 0000000..d4b2515 --- /dev/null +++ b/app/funnelView/stepThreeView.tsx @@ -0,0 +1,155 @@ +import React from 'react'; +import { + Layout, + StyleService, + useStyleSheet, + Text, + Button, +} from '@ui-kitten/components'; +import { View, TouchableOpacity } from 'react-native'; +import { scale, verticalScale, moderateScale } from 'react-native-size-matters'; +import * as Animatable from 'react-native-animatable'; + +const JobSelectionScreen = ({ + onNext, + currentStep = 2, + totalSteps = 5, + selectedJob, + setSelectedJob, +}) => { + const styles = useStyleSheet(themedStyles); + const [selectedItem, setSelectedItem] = React.useState(selectedJob); + + const JobRanges = ['중·고등학생', '대학생', '직장인', '자영업자', '기타']; + + const renderStepIndicators = () => { + return ( + + {[...Array(totalSteps)].map((_, index) => ( + + ))} + + ); + }; + + const renderJobOption = (job, index) => { + const isSelected = selectedItem === index; + + return ( + setSelectedItem(index)} + style={[styles.ageOption, isSelected && styles.selectedAgeOption]} + > + + {job} + + + ); + }; + + return ( + + {renderStepIndicators()} + + 직업을 알려주세요 + + + + {JobRanges.map((age, index) => renderJobOption(age, index))} + + + {selectedItem !== null && ( + + + + )} + + + ); +}; + +const themedStyles = StyleService.create({ + container: { + flex: 1, + backgroundColor: 'background-basic-color-1', + paddingHorizontal: scale(20), + }, + stepContainer: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + marginBottom: verticalScale(40), + }, + stepIndicator: { + width: scale(8), + height: scale(8), + borderRadius: scale(4), + backgroundColor: 'color-basic-400', + marginHorizontal: scale(3), + }, + activeStepIndicator: { + backgroundColor: 'color-primary-500', + width: scale(16), + height: scale(8), + }, + title: { + textAlign: 'left', + marginBottom: verticalScale(30), + fontSize: moderateScale(28), + fontWeight: 'bold', + color: 'text-basic-color', + }, + optionsContainer: {}, + ageOption: { + backgroundColor: 'background-basic-color-2', + padding: moderateScale(16), + borderRadius: moderateScale(8), + marginBottom: verticalScale(10), + }, + selectedAgeOption: { + backgroundColor: 'color-primary-100', + }, + ageText: { + fontSize: moderateScale(16), + color: 'text-basic-color', + }, + selectedAgeText: { + color: 'color-primary-700', + fontWeight: 'bold', + }, + button: { + borderRadius: moderateScale(8), + }, + buttonContainer: {}, + contentContainer: { + flex: 1, + justifyContent: 'space-between', + paddingBottom: verticalScale(20), // 하단 간격 + }, +}); + +export default JobSelectionScreen; diff --git a/app/funnelView/stepTwoView.tsx b/app/funnelView/stepTwoView.tsx new file mode 100644 index 0000000..a233096 --- /dev/null +++ b/app/funnelView/stepTwoView.tsx @@ -0,0 +1,155 @@ +import React from 'react'; +import { + Layout, + StyleService, + useStyleSheet, + Text, + Button, +} from '@ui-kitten/components'; +import { View, TouchableOpacity } from 'react-native'; +import { scale, verticalScale, moderateScale } from 'react-native-size-matters'; +import * as Animatable from 'react-native-animatable'; + +const AgeSelectionScreen = ({ + onNext, + currentStep = 1, + totalSteps = 5, + selectedAge, + setSelectedAge, +}) => { + const styles = useStyleSheet(themedStyles); + const [selectedItem, setSelectedItem] = React.useState(selectedAge); + + const ageRanges = ['10대', '20대', '30대', '40대', '50대 이상']; + + const renderStepIndicators = () => { + return ( + + {[...Array(totalSteps)].map((_, index) => ( + + ))} + + ); + }; + + const renderAgeOption = (age, index) => { + const isSelected = selectedItem === index; + + return ( + setSelectedItem(index)} + style={[styles.ageOption, isSelected && styles.selectedAgeOption]} + > + + {age} + + + ); + }; + + return ( + + {renderStepIndicators()} + + 연령대를 알려주세요 + + + + {ageRanges.map((age, index) => renderAgeOption(age, index))} + + + {selectedItem !== null && ( + + + + )} + + + ); +}; + +const themedStyles = StyleService.create({ + container: { + flex: 1, + backgroundColor: 'background-basic-color-1', + paddingHorizontal: scale(20), + }, + stepContainer: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + marginBottom: verticalScale(40), + }, + stepIndicator: { + width: scale(8), + height: scale(8), + borderRadius: scale(4), + backgroundColor: 'color-basic-400', + marginHorizontal: scale(3), + }, + activeStepIndicator: { + backgroundColor: 'color-primary-500', + width: scale(16), + height: scale(8), + }, + title: { + textAlign: 'left', + marginBottom: verticalScale(30), + fontSize: moderateScale(28), + fontWeight: 'bold', + color: 'text-basic-color', + }, + optionsContainer: {}, + ageOption: { + backgroundColor: 'background-basic-color-2', + padding: moderateScale(16), + borderRadius: moderateScale(8), + marginBottom: verticalScale(10), + }, + selectedAgeOption: { + backgroundColor: 'color-primary-100', + }, + ageText: { + fontSize: moderateScale(16), + color: 'text-basic-color', + }, + selectedAgeText: { + color: 'color-primary-700', + fontWeight: 'bold', + }, + button: { + borderRadius: moderateScale(8), + }, + buttonContainer: {}, + contentContainer: { + flex: 1, + justifyContent: 'space-between', + paddingBottom: verticalScale(20), // 하단 간격 + }, +}); + +export default AgeSelectionScreen; diff --git a/components/common/molecules/ConfirmModal.tsx b/components/common/molecules/ConfirmModal.tsx index 93da1f4..aebb0a6 100644 --- a/components/common/molecules/ConfirmModal.tsx +++ b/components/common/molecules/ConfirmModal.tsx @@ -2,10 +2,7 @@ import React from 'react'; import { Modal, Layout, Text, Button, useTheme } from '@ui-kitten/components'; import { StyleSheet, View } from 'react-native'; import { useTranslation } from 'react-i18next'; -import { - heightPercentage, - widthPercentage, -} from '../../../utils/responsiveSize'; +import { scale, verticalScale } from 'react-native-size-matters'; import fontStyles from '../../../theme/fontStyles'; interface ConfirmModalProps { @@ -67,10 +64,21 @@ const ConfirmModal: React.FC = ({ status="basic" onPress={() => onCancel()} > - {t(cancelTextKey)} + + {t(cancelTextKey)} + @@ -83,8 +91,8 @@ const styles = StyleSheet.create({ backgroundColor: 'rgba(0, 0, 0, 0.5)', }, card: { - height: heightPercentage(165), - width: widthPercentage(328), + height: verticalScale(165), + width: scale(328), flexDirection: 'column', alignItems: 'center', justifyContent: 'center', @@ -95,27 +103,23 @@ const styles = StyleSheet.create({ borderRadius: 16, }, textContainer: { - height: heightPercentage(49), - width: widthPercentage(296), + width: scale(296), flexDirection: 'column', alignItems: 'center', justifyContent: 'space-between', gap: 8, }, - text: { - height: heightPercentage(23), - }, + text: {}, buttonContainer: { - height: heightPercentage(52), - width: widthPercentage(296), + width: scale(296), flexDirection: 'row', justifyContent: 'space-around', alignItems: 'center', gap: 12, }, button: { - width: widthPercentage(142), - height: heightPercentage(52), + width: scale(142), + height: verticalScale(52), alignItems: 'center', justifyContent: 'center', borderRadius: 12, diff --git a/hooks/auth/useLogin.js b/hooks/auth/useLogin.js index 2617300..bc8be1e 100644 --- a/hooks/auth/useLogin.js +++ b/hooks/auth/useLogin.js @@ -32,8 +32,11 @@ const useLogin = () => { await setAsyncStorageLoginInfo(jwtTokenData, user); setUserId(jwtTokenData.userId); setIsLoggedIn(true); - - router.push('/(tabs)'); + if (jwtTokenData.isNew) { + router.push('/funnelView/funnelView'); + } else { + router.push('/(tabs)'); + } } }; diff --git a/theme/fontStyles.js b/theme/fontStyles.js index 23575ff..69e5e1b 100644 --- a/theme/fontStyles.js +++ b/theme/fontStyles.js @@ -16,114 +16,96 @@ const fontStyles = { H1: { B_130: { fontFamily: BOLD, - fontSize: 28, - lineHeight: Math.round(28 * 1.3) + 2, + fontSize: moderateScale(28), }, B_100: { fontFamily: BOLD, - fontSize: 28, - lineHeight: 30, + fontSize: moderateScale(28), }, M_130: { fontFamily: MEDIUM, - fontSize: 28, - lineHeight: Math.round(28 * 1.3) + 2, + fontSize: moderateScale(28), }, M_100: { fontFamily: MEDIUM, - fontSize: 28, - lineHeight: 30, + fontSize: moderateScale(28), }, R_130: { fontFamily: REGULAR, - fontSize: 28, - lineHeight: Math.round(28 * 1.3) + 2, + fontSize: moderateScale(28), }, R_100: { fontFamily: REGULAR, - fontSize: 28, - lineHeight: 30, + fontSize: moderateScale(28), }, }, H2: { B_130: { fontFamily: BOLD, - fontSize: 24, - lineHeight: Math.round(24 * 1.3) + 2, + fontSize: moderateScale(24), }, B_100: { fontFamily: BOLD, - fontSize: 24, - lineHeight: 26, + fontSize: moderateScale(24), }, M_130: { fontFamily: MEDIUM, - fontSize: 24, - lineHeight: Math.round(24 * 1.3) + 2, + fontSize: moderateScale(24), }, M_100: { fontFamily: MEDIUM, - fontSize: 24, - lineHeight: 26, + fontSize: moderateScale(24), }, R_130: { fontFamily: REGULAR, - fontSize: 24, - lineHeight: Math.round(24 * 1.3) + 2, + fontSize: moderateScale(24), }, R_100: { fontFamily: REGULAR, - fontSize: 24, - lineHeight: 26, + fontSize: moderateScale(24), }, }, H3: { B_130: { fontFamily: BOLD, - fontSize: 20, - lineHeight: Math.round(20 * 1.3) + 2, + fontSize: moderateScale(20), }, B_100: { fontFamily: BOLD, - fontSize: 20, - lineHeight: 22, + fontSize: moderateScale(20), }, M_130: { fontFamily: MEDIUM, - fontSize: 20, - lineHeight: Math.round(20 * 1.3) + 2, + fontSize: moderateScale(20), }, M_100: { fontFamily: MEDIUM, - fontSize: 20, - lineHeight: 22, + fontSize: moderateScale(20), }, R_130: { fontFamily: REGULAR, - fontSize: 20, - lineHeight: Math.round(20 * 1.3) + 2, + fontSize: moderateScale(20), }, R_100: { fontFamily: REGULAR, - fontSize: 20, - lineHeight: 22, + fontSize: moderateScale(20), }, }, }, @@ -132,76 +114,64 @@ const fontStyles = { S1: { B_130: { fontFamily: BOLD, - fontSize: 18, - lineHeight: Math.round(18 * 1.3) + 2, + fontSize: moderateScale(18), }, B_100: { fontFamily: BOLD, - fontSize: 18, - lineHeight: 20, + fontSize: moderateScale(18), }, M_130: { fontFamily: MEDIUM, - fontSize: 18, - lineHeight: Math.round(18 * 1.3) + 2, + fontSize: moderateScale(18), }, M_100: { fontFamily: MEDIUM, - fontSize: 18, - lineHeight: 20, + fontSize: moderateScale(18), }, R_130: { fontFamily: REGULAR, - fontSize: 18, - lineHeight: Math.round(18 * 1.3) + 2, + fontSize: moderateScale(18), }, R_100: { fontFamily: REGULAR, - fontSize: 18, - lineHeight: 20, + fontSize: moderateScale(18), }, }, S2: { B_130: { fontFamily: BOLD, - fontSize: 16, - lineHeight: Math.round(16 * 1.3) + 2, + fontSize: moderateScale(16), }, B_100: { fontFamily: BOLD, - fontSize: 16, - lineHeight: 18, + fontSize: moderateScale(16), }, M_130: { fontFamily: MEDIUM, - fontSize: 16, - lineHeight: Math.round(16 * 1.3) + 2, + fontSize: moderateScale(16), }, M_100: { fontFamily: MEDIUM, - fontSize: 16, - lineHeight: 18, + fontSize: moderateScale(30), }, R_130: { fontFamily: REGULAR, - fontSize: 16, - lineHeight: Math.round(16 * 1.3) + 2, + fontSize: moderateScale(16), }, R_100: { fontFamily: REGULAR, - fontSize: 16, - lineHeight: 18, + fontSize: moderateScale(16), }, }, }, @@ -210,75 +180,64 @@ const fontStyles = { P1: { B_130: { fontFamily: BOLD, - fontSize: 14, - lineHeight: Math.round(14 * 1.3) + 2, + fontSize: moderateScale(14), }, B_100: { fontFamily: BOLD, - fontSize: 14, - lineHeight: 16, + fontSize: moderateScale(14), }, M_130: { fontFamily: MEDIUM, - fontSize: 14, - lineHeight: Math.round(14 * 1.3) + 2, + fontSize: moderateScale(14), }, M_100: { fontFamily: MEDIUM, - fontSize: moderateScale(14, 0.3), + fontSize: moderateScale(14), }, R_130: { fontFamily: REGULAR, - fontSize: 14, - lineHeight: Math.round(14 * 1.3) + 2, + fontSize: moderateScale(14), }, R_100: { fontFamily: REGULAR, - fontSize: 14, - lineHeight: 16, + fontSize: moderateScale(14), }, }, P2: { B_130: { fontFamily: BOLD, - fontSize: 12, - lineHeight: Math.round(12 * 1.3) + 2, + fontSize: moderateScale(12), }, B_100: { fontFamily: BOLD, - fontSize: 12, - lineHeight: 14, + fontSize: moderateScale(12), }, M_130: { fontFamily: MEDIUM, - fontSize: 12, - lineHeight: Math.round(12 * 1.3) + 2, + fontSize: moderateScale(12), }, M_100: { fontFamily: MEDIUM, - fontSize: 12, - lineHeight: 14, + fontSize: moderateScale(12), }, R_130: { fontFamily: REGULAR, - fontSize: 12, - lineHeight: Math.round(12 * 1.3) + 2, + fontSize: moderateScale(12), }, R_100: { fontFamily: REGULAR, - fontSize: 12, - lineHeight: 14, + fontSize: moderateScale(12), }, }, }, @@ -286,20 +245,17 @@ const fontStyles = { Caption: { B_130: { fontFamily: BOLD, - fontSize: 10, - lineHeight: Math.round(10 * 1.3) + 2, + fontSize: moderateScale(10), }, M_130: { fontFamily: MEDIUM, - fontSize: 10, - lineHeight: Math.round(10 * 1.3) + 2, + fontSize: moderateScale(10), }, R_130: { fontFamily: REGULAR, - fontSize: 10, - lineHeight: Math.round(10 * 1.3) + 2, + fontSize: moderateScale(10), }, }, }; diff --git a/theme/mapping.json b/theme/mapping.json index a435608..2f1f04e 100644 --- a/theme/mapping.json +++ b/theme/mapping.json @@ -243,6 +243,15 @@ } } } + }, + "Button": { + "appearances": { + "variantGroups": { + "size": { + "large": {} + } + } + } } } } diff --git a/theme/theme.json b/theme/theme.json index 49dfd95..e711c77 100644 --- a/theme/theme.json +++ b/theme/theme.json @@ -1,13 +1,18 @@ { - "color-primary-100": "#D6E4FF", - "color-primary-200": "#ADC8FF", - "color-primary-300": "#84A9FF", - "color-primary-400": "#6690FF", - "color-primary-500": "#3366FF", - "color-primary-600": "#254EDB", - "color-primary-700": "#1939B7", - "color-primary-800": "#102693", - "color-primary-900": "#091A7A", + "color-primary-100": "#E6F0FF", + "color-primary-200": "#B3D6FF", + "color-primary-300": "#80BBFF", + "color-primary-400": "#4DA1FF", + "color-primary-500": "#0578FF", + "color-primary-600": "#0460CC", + "color-primary-700": "#034899", + "color-primary-800": "#023066", + "color-primary-900": "#011833", + + "background-basic-color-1": "#FFFFFF", + "background-basic-color-2": "#F4F6F8", + "background-basic-color-3": "#E8EBF0", + "background-basic-color-4": "#D9DEE4", "Black01": "#111111", "Black02": "#28323C",