diff --git a/assets/icons/section/caring_dog.svg b/assets/icons/section/caring_dog.svg new file mode 100644 index 0000000..5208356 --- /dev/null +++ b/assets/icons/section/caring_dog.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/app/navigation/RootNavigator.tsx b/src/app/navigation/RootNavigator.tsx index 17453a7..fd06fc1 100644 --- a/src/app/navigation/RootNavigator.tsx +++ b/src/app/navigation/RootNavigator.tsx @@ -1,37 +1,48 @@ import React from 'react'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; -import { useSessionStore } from '@shared/store/useSessionStore'; -import LoginScreen from '@screens/auth/Login/LoginScreen'; -import SignupScreen from '@screens/auth/Signup/SignupScreen'; -import { LinkAccountScreen } from '@screens/auth/LinkAccount/LinkAccountScreen'; + +import { useSessionStore } from '../../shared/store/useSessionStore'; +import LoginScreen from '../../screens/auth/Login/LoginScreen'; +import SignupScreen from '../../screens/auth/Signup/component/SignupScreen'; +import { SignupTypeSelectScreen } from '../../screens/auth/Signup/component/SignupTypeSelectScreen'; +import TermsAgreementScreen from '../../screens/auth/terms/TermsAgreementScreen'; +import { LinkAccountScreen } from '../../screens/auth/LinkAccount/LinkAccountScreen'; import { GuardianStackNavigator } from './GuardianStackNavigator'; -// 👇 어르신 메인 화면 컴포넌트 경로를 올바른 메인 화면으로 지정해주세요 (예: SeniorHomeTabs 또는 SeniorMainScreen 등) -import SeniorHomeScreen from '@screens/senior/Home/SeniorHomeScreen'; +import SeniorHomeScreen from '../../screens/senior/Home/SeniorHomeScreen'; +import { SeniorScheduleScreen } from '../../screens/senior/Schedule/SeniorScheduleScreen'; const Stack = createNativeStackNavigator(); -export function RootNavigator() { +export const RootNavigator = () => { const isLoggedIn = useSessionStore(state => state.isLoggedIn); const role = useSessionStore(state => state.role); + // 1. 보호자(PROTECTOR)로 로그인된 경우 if (isLoggedIn && role === 'PROTECTOR') { return ; } + // 2. 피보호자/어르신(WARD)으로 로그인된 경우 if (isLoggedIn && role === 'WARD') { return ( - {/* 어르신으로 진입 시 보여질 메인 화면 컴포넌트 연결 */} + {/* 필요한 경우 어르신용 추가 스크린을 여기에 배치할 수 있습니다 */} ); } + // 3. 비로그인 상태 (인증 및 회원가입 관련 스크린 제공) return ( - + + + + + + ); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/screens/auth/Login/LoginScreen.tsx b/src/screens/auth/Login/LoginScreen.tsx index d1b5fe3..10d4233 100644 --- a/src/screens/auth/Login/LoginScreen.tsx +++ b/src/screens/auth/Login/LoginScreen.tsx @@ -1,174 +1,214 @@ import React, { useState } from 'react'; -import { StyleSheet, Text, View, TextInput, TouchableOpacity } from 'react-native'; -import { useSessionStore } from '@shared/store/useSessionStore'; +import { SafeAreaView, View, Text, TextInput, TouchableOpacity, ScrollView, StyleSheet } from 'react-native'; +import { useSessionStore } from '../../../shared/store/useSessionStore'; -export default function LoginScreen() { +export default function LoginScreen({ navigation }: { navigation: any }) { const [id, setId] = useState(''); const [password, setPassword] = useState(''); - return ( - - + const handleLogin = () => { + console.log('로그인 시도:', id, password); + }; + + // [DEV] 보호자 진입 핸들러 + const handleDevProtector = () => { + useSessionStore.setState({ isLoggedIn: true, role: 'PROTECTOR' }); + }; - {__DEV__ && ( - - useSessionStore.getState().login('PROTECTOR')}> - [DEV] 보호자로 바로 진입 + // [DEV] 어르신 진입 핸들러 + const handleDevSenior = () => { + useSessionStore.setState({ isLoggedIn: true, role: 'WARD' }); + }; + + return ( + + + + {/* 상단 [DEV] 버튼 영역 */} + + + [DEV] 보호자로 바로 진입 - useSessionStore.getState().login('WARD')}> - [DEV] 어르신으로 바로 진입 + + [DEV] 어르신으로 바로 진입 - )} - 돌봄의 시작, 케어링 - Caring + {/* 타이틀 영역 */} + + 돌봄의 시작, 케어링 + Caring + + + {/* 로그인 입력 폼 */} + + - - - - + - - 로그인 - + + 로그인 + - - 아이디 찾기 - | - 비밀번호 찾기 - | - 회원가입 - + {/* 하단 링크 영역 */} + + + 아이디 찾기 + + + | + + + 비밀번호 찾기 + + + | + + navigation.navigate('SignupTypeSelect')}> + 회원가입 + + + - - 간편 로그인 - - - - + {/* 간편 로그인 원형 버튼 영역 */} + + 간편 로그인 + + + + + - - + + + ); } -const styles = StyleSheet.create({ - container: { +const loginStyles = StyleSheet.create({ + safeArea: { flex: 1, backgroundColor: '#FFFFFF', - paddingHorizontal: 24, + }, + scrollContainer: { + padding: 20, + flexGrow: 1, + justifyContent: 'center', + }, + devButtonContainer: { + marginBottom: 20, + gap: 8, + }, + devButton: { + borderWidth: 1, + borderColor: '#FF7F00', + borderRadius: 8, + paddingVertical: 10, alignItems: 'center', }, - spacer: { - flex: 0.5, + devButtonText: { + color: '#FF7F00', + fontSize: 13, + fontWeight: '600', + }, + titleContainer: { + alignItems: 'center', + marginBottom: 30, }, subTitle: { - fontSize: 26, - fontWeight: 'bold', - color: '#333333', - marginBottom: 8, + fontSize: 16, + color: '#666', + marginBottom: 5, }, mainTitle: { - fontSize: 56, + fontSize: 32, fontWeight: 'bold', - color: '#FD7E14', - marginBottom: 40, + color: '#FF7F00', }, - inputContainer: { + formContainer: { width: '100%', - marginBottom: 20, + marginBottom: 30, }, input: { width: '100%', height: 50, borderWidth: 1, - borderColor: '#E0E0E0', + borderColor: '#DDD', borderRadius: 8, paddingHorizontal: 15, - marginBottom: 12, + fontSize: 15, backgroundColor: '#FAFAFA', + marginBottom: 12, + color: '#000', }, loginButton: { width: '100%', height: 50, - backgroundColor: '#FD7E14', + backgroundColor: '#FF7F00', + borderRadius: 8, justifyContent: 'center', alignItems: 'center', - borderRadius: 8, - marginBottom: 20, + marginTop: 8, }, loginButtonText: { - color: '#FFFFFF', + color: '#FFF', fontSize: 16, fontWeight: 'bold', }, - findMenuContainer: { + linkRow: { flexDirection: 'row', + justifyContent: 'center', alignItems: 'center', - marginBottom: 40, + marginTop: 15, }, - findMenuText: { - fontSize: 12, - color: '#666666', + linkText: { + color: '#666', + fontSize: 13, }, - divider: { + barText: { + color: '#DDD', marginHorizontal: 10, - color: '#CCCCCC', - fontSize: 12, }, socialContainer: { - width: '100%', alignItems: 'center', }, socialTitle: { - fontSize: 12, - color: '#999999', - marginBottom: 15, - }, - socialButtonsRow: { - flexDirection: 'row', - justifyContent: 'center', - gap: 20, - }, - circleButton: { - width: 44, - height: 44, - borderRadius: 22, - }, - devRow: { - width: '100%', - gap: 8, + fontSize: 13, + color: '#888', marginBottom: 12, }, - devButton: { - width: '100%', - paddingVertical: 10, - borderRadius: 8, - borderWidth: 1, - borderColor: '#FD7E14', - alignItems: 'center', + socialIconRow: { + flexDirection: 'row', + gap: 15, }, - devButtonText: { - color: '#FD7E14', - fontSize: 13, - fontWeight: '600', + socialCircle: { + width: 40, + height: 40, + borderRadius: 20, }, -}); +}); \ No newline at end of file diff --git a/src/screens/auth/Signup/component/BirthDatePicker.tsx b/src/screens/auth/Signup/component/BirthDatePicker.tsx new file mode 100644 index 0000000..1839150 --- /dev/null +++ b/src/screens/auth/Signup/component/BirthDatePicker.tsx @@ -0,0 +1,167 @@ +import React, { useState, useEffect } from 'react'; +import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; +import { Calendar, LocaleConfig } from 'react-native-calendars'; +import { BirthDatePickerProps } from '../types/birthDatePicker.types'; + +// 달력 한국어 설정 +LocaleConfig.locales['kr'] = { + monthNames: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'], + monthNamesShort: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'], + dayNames: ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'], + dayNamesShort: ['일', '월', '화', '수', '목', '금', '토'], + today: '오늘', +}; +LocaleConfig.defaultLocale = 'kr'; + +// YYYY-MM-DD 포맷 변환 함수 +const formatDate = (date: Date) => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +}; + +export default function BirthDatePicker({ value, onChange }: BirthDatePickerProps) { + // 선택된 날짜 문자열 관리 + const [selectedDate, setSelectedDate] = useState(value || ''); + + // 외부 props(value) 변경 시 내부 상태 동기화 + useEffect(() => { + if (value) { + setSelectedDate(value); + } + }, [value]); + + // 달력 표시 기준 Date 객체 + const [currentDate, setCurrentDate] = useState(() => { + if (value) { + const parsed = new Date(value); + if (!isNaN(parsed.getTime())) return parsed; + } + return new Date(); + }); + + // 연도 변경 (-1년 / +1년) + const changeYear = (amount: number) => { + const nextDate = new Date(currentDate); + nextDate.setFullYear(nextDate.getFullYear() + amount); + setCurrentDate(nextDate); + }; + + // 월 변경 (-1달 / +1달) + const changeMonth = (amount: number) => { + const nextDate = new Date(currentDate); + nextDate.setMonth(nextDate.getMonth() + amount); + setCurrentDate(nextDate); + }; + + const formattedDateString = formatDate(currentDate); + const currentYear = currentDate.getFullYear(); + const currentMonth = currentDate.getMonth() + 1; + + // 날짜 클릭 이벤트 + const handleDayPress = (day: { dateString: string }) => { + const newDateStr = day.dateString; + setSelectedDate(newDateStr); + if (onChange) { + onChange(newDateStr); + } + }; + + return ( + + {/* 상단 고정 커스텀 헤더 (년도 및 월 조작) */} + + {/* 이전 연도 / 이전 월 버튼 */} + + changeYear(-1)} style={customStyles.arrowBtn}> + « + + changeMonth(-1)} style={customStyles.arrowBtn}> + + + + + {/* 현재 년도 및 월 표시 */} + {`${currentYear}년 ${currentMonth}월`} + + {/* 다음 월 / 다음 연도 버튼 */} + + changeMonth(1)} style={customStyles.arrowBtn}> + + + changeYear(1)} style={customStyles.arrowBtn}> + » + + + + + {/* 고정 달력 */} + null} + onDayPress={handleDayPress} + theme={{ + todayTextColor: '#FF7A00', + }} + /> + + ); +} + +const customStyles = StyleSheet.create({ + container: { + borderWidth: 1, + borderColor: '#E2E8F0', + borderRadius: 12, + overflow: 'hidden', + backgroundColor: '#fff', + marginTop: 4, + }, + headerContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 12, + paddingVertical: 10, + backgroundColor: '#fff', + borderBottomWidth: 1, + borderBottomColor: '#eee', + }, + btnGroup: { + flexDirection: 'row', + alignItems: 'center', + }, + headerTitle: { + fontSize: 16, + fontWeight: 'bold', + color: '#333', + }, + arrowBtn: { + paddingHorizontal: 6, + paddingVertical: 2, + }, + yearBtnText: { + fontSize: 20, + fontWeight: 'bold', + color: '#FF7A00', + }, + monthBtnText: { + fontSize: 22, + fontWeight: 'bold', + color: '#555', + }, +}); \ No newline at end of file diff --git a/src/screens/auth/Signup/component/CaringDogIcon.tsx b/src/screens/auth/Signup/component/CaringDogIcon.tsx new file mode 100644 index 0000000..1a771b5 --- /dev/null +++ b/src/screens/auth/Signup/component/CaringDogIcon.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { SvgXml } from 'react-native-svg'; + +// 피그마에서 [Copy as SVG]로 복사한 전체 코드 붙여넣기 +const dogXml = ` + + + + + + + + +`; + +interface CaringDogIconProps { + width?: number; + height?: number; +} + +export const CaringDogIcon = ({ width = 180, height = 180 }: CaringDogIconProps) => { + return ; +}; + +export default CaringDogIcon; \ No newline at end of file diff --git a/src/screens/auth/Signup/component/CaringLogo.tsx b/src/screens/auth/Signup/component/CaringLogo.tsx new file mode 100644 index 0000000..b90072b --- /dev/null +++ b/src/screens/auth/Signup/component/CaringLogo.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { SvgXml } from 'react-native-svg'; + +// 피그마에서 케어링 로고 [Copy as SVG]한 코드 또는 로고 SVG 코드 +const logoXml = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +interface CaringLogoProps { + width?: number; + height?: number; +} + +export const CaringLogo = ({ width = 44, height = 44 }: CaringLogoProps) => { + return ; +}; + +export default CaringLogo; \ No newline at end of file diff --git a/src/screens/auth/Signup/component/RoleSelectButton.tsx b/src/screens/auth/Signup/component/RoleSelectButton.tsx new file mode 100644 index 0000000..7392862 --- /dev/null +++ b/src/screens/auth/Signup/component/RoleSelectButton.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { TouchableOpacity, Text } from 'react-native'; +import { signupTypeSelectStyles } from '../styles/signupTypeSelect.styles'; + +interface RoleSelectButtonProps { + title: string; + onPress: () => void; +} + +export default function RoleSelectButton({ title, onPress }: RoleSelectButtonProps) { + return ( + + {title} + + ); +} \ No newline at end of file diff --git a/src/screens/auth/Signup/component/SignupScreen.tsx b/src/screens/auth/Signup/component/SignupScreen.tsx new file mode 100644 index 0000000..af8734d --- /dev/null +++ b/src/screens/auth/Signup/component/SignupScreen.tsx @@ -0,0 +1,235 @@ +import React from 'react'; +import { + View, + Text, + TextInput, + TouchableOpacity, + ScrollView, + SafeAreaView, +} from 'react-native'; + +import { signupStyles } from '../styles/signup.styles'; +import useSignUp from '../hooks/useSignUp'; +import BirthDatePicker from './BirthDatePicker'; + +// 로고 SVG import +import LogoIcon from '@assets/icons/header/caring_logo.svg'; + +export const SignupScreen = () => { + const signUpHook = useSignUp() as any; + + // 훅 상태 및 핸들러 추출 + const form = signUpHook?.form || {}; + const setName = signUpHook?.setName || signUpHook?.handleNameChange; + const setPhone = signUpHook?.setPhone || signUpHook?.handlePhoneChange; + const setAuthCode = signUpHook?.setAuthCode || signUpHook?.handleAuthCodeChange; + const setPassword = signUpHook?.setPassword || signUpHook?.handlePasswordChange; + const setConfirmPassword = signUpHook?.setConfirmPassword || signUpHook?.handleConfirmPasswordChange; + + const handleSendAuthCode = signUpHook?.handleSendAuthCode || (() => {}); + const handleVerifyAuthCode = signUpHook?.handleVerifyAuthCode || (() => {}); + + const setAddress = signUpHook?.setAddress; + const setDetailAddress = signUpHook?.setDetailAddress; + const handleSelectAddress = signUpHook?.handleSelectAddress; + const handleBirthDateChange = signUpHook?.setBirthDate || signUpHook?.handleBirthDateChange; + + const toggleDisease = signUpHook?.toggleDisease || signUpHook?.handleToggleDisease || (() => {}); + const handleSubmit = signUpHook?.handleSubmit || signUpHook?.handleSignUp || signUpHook?.onSubmit || (() => {}); + const isFormValid = signUpHook?.isFormValid ?? true; + + // 기저질환 목록 + const diseaseList: string[] = signUpHook?.diseaseList || signUpHook?.diseases || [ + '고혈압', + '당뇨병', + '치매', + '골다공증', + '고지혈증', + '관절염', + '심혈관 질환', + '만성 신부전', + '파킨슨병', + '기타', + ]; + + const selectedDiseases: string[] = form?.selectedDiseases || signUpHook?.selectedDiseases || []; + + return ( + + + + {/* 상단 헤더 영역: 로고와 타이틀을 독립된 구조로 분리 */} + + + + + 회원가입 + + + {/* 1. 이름 입력 */} + {form?.name !== undefined && ( + + 이름 + + + )} + + {/* 2. 전화번호 입력 & 인증번호 요청 */} + {form?.phone !== undefined && ( + + 전화번호 + + + + 인증요청 + + + + )} + + {/* 3. 인증번호 입력 & 확인 */} + + 인증번호 + + + + 인증확인 + + + + + {/* 4. 비밀번호 입력 */} + + 비밀번호 + + + + {/* 5. 비밀번호 확인 입력 */} + + 비밀번호 확인 + + {form?.password && form?.confirmPassword && form.password !== form.confirmPassword && ( + + 비밀번호가 일치하지 않습니다. + + )} + + + {/* 6. 생년월일 선택 */} + + 생년월일 + { + if (handleBirthDateChange) { + handleBirthDateChange(date); + } + }} + /> + + + {/* 7. 주소 입력 */} + + 주소 + + + + + + + {/* 8. 기저 질환 선택 */} + + 기저 질환 선택 + + + {diseaseList.map((disease: string) => { + const isSelected = selectedDiseases.includes(disease); + return ( + + toggleDisease(disease)} + activeOpacity={0.7} + > + + {isSelected && } + + {disease} + + + ); + })} + + + + {/* 9. 제출 버튼 */} + + 가입하기 + + + + + ); +}; + +export default SignupScreen; \ No newline at end of file diff --git a/src/screens/auth/Signup/component/SignupTypeSelectScreen.tsx b/src/screens/auth/Signup/component/SignupTypeSelectScreen.tsx new file mode 100644 index 0000000..1b8ba60 --- /dev/null +++ b/src/screens/auth/Signup/component/SignupTypeSelectScreen.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { + View, + Text, + TouchableOpacity, + SafeAreaView, +} from 'react-native'; +import { useNavigation } from '@react-navigation/native'; + +import { signupTypeSelectStyles } from '../styles/signupTypeSelect.styles'; +import { useSignupTypeSelect } from '../hooks/useSignupTypeSelect'; + +import { CaringLogo } from './CaringLogo'; +import { CaringDogIcon } from './CaringDogIcon'; + +export const SignupTypeSelectScreen = () => { + const navigation = useNavigation(); + const typeSelectHook = useSignupTypeSelect(navigation) as any; + + const handleSelectType = + typeSelectHook?.handleRoleSelect || + typeSelectHook?.handleSelectType || + typeSelectHook?.onSelectType; + + return ( + + {/* 1. 상단 케어링 로고 */} + + + + + + {/* 2. 중앙 컨텐츠 영역 (강아지 + 텍스트 + 버튼) */} + + {/* 강아지 캐릭터 */} + + + + + {/* 메인 안내 문구 (강아지 발 바로 아래) */} + 안녕하세요! + + 어떤 서비스를 이용하시나요? + + + {/* 3. 역할 선택 버튼 영역 */} + + handleSelectType && handleSelectType('GUARDIAN')} + activeOpacity={0.7} + > + 보호자 + + + handleSelectType && handleSelectType('SENIOR')} + activeOpacity={0.7} + > + 돌봄대상자 + + + + + ); +}; + +export default SignupTypeSelectScreen; \ No newline at end of file diff --git a/src/screens/auth/Signup/hooks/useBirthDatePicker.ts b/src/screens/auth/Signup/hooks/useBirthDatePicker.ts new file mode 100644 index 0000000..004ce0e --- /dev/null +++ b/src/screens/auth/Signup/hooks/useBirthDatePicker.ts @@ -0,0 +1,21 @@ +import { useState } from 'react'; + +export function useBirthDatePicker(initialDate: string, onChange: (date: string) => void) { + const [isOpen, setIsOpen] = useState(true); + + const handleDayPress = (day: { dateString: string }) => { + onChange(day.dateString); + // 선택 후 닫고 싶다면 아래 주석을 해제하세요 + // setIsOpen(false); + }; + + const toggleOpen = () => setIsOpen(prev => !prev); + const closeCalendar = () => setIsOpen(false); + + return { + isOpen, + toggleOpen, + closeCalendar, + handleDayPress, + }; +} \ No newline at end of file diff --git a/src/screens/auth/Signup/hooks/useSignUp.ts b/src/screens/auth/Signup/hooks/useSignUp.ts new file mode 100644 index 0000000..695b6f4 --- /dev/null +++ b/src/screens/auth/Signup/hooks/useSignUp.ts @@ -0,0 +1,58 @@ +import { useState } from 'react'; + +export default function useSignUp() { + // 입력 폼 상태 관리 + const [name, setName] = useState(''); + const [phone, setPhone] = useState(''); + const [authCode, setAuthCode] = useState(''); + const [password, setPassword] = useState(''); + const [passwordConfirm, setPasswordConfirm] = useState(''); + const [birthDate, setBirthDate] = useState(''); + const [address, setAddress] = useState(''); + const [selectedDiseases, setSelectedDiseases] = useState([]); + + // 주소 모달 상태 + const [isAddressModalOpen, setIsAddressModalOpen] = useState(false); + + // 기저 질환 체크박스 토글 함수 + const toggleDisease = (disease: string) => { + if (selectedDiseases.includes(disease)) { + setSelectedDiseases(selectedDiseases.filter((item) => item !== disease)); + } else { + setSelectedDiseases([...selectedDiseases, disease]); + } + }; + + // 다음 주소 검색 선택 핸들러 + const handleSelectAddress = (data: any) => { + setAddress(data.address); + setIsAddressModalOpen(false); + }; + + return { + // 상태값 모음 + form: { + name, + phone, + authCode, + password, + passwordConfirm, + birthDate, + address, + selectedDiseases, + }, + // Setters + setName, + setPhone, + setAuthCode, + setPassword, + setPasswordConfirm, + setBirthDate, + setAddress, + // 모달 및 핸들러 + isAddressModalOpen, + setIsAddressModalOpen, + toggleDisease, + handleSelectAddress, + }; +} \ No newline at end of file diff --git a/src/screens/auth/Signup/hooks/useSignupTypeSelect.ts b/src/screens/auth/Signup/hooks/useSignupTypeSelect.ts new file mode 100644 index 0000000..80822c9 --- /dev/null +++ b/src/screens/auth/Signup/hooks/useSignupTypeSelect.ts @@ -0,0 +1,12 @@ +import { UserRole } from '../types/signuptype.types'; + +export function useSignupTypeSelect(navigation: any) { + const handleRoleSelect = (role: UserRole) => { + // 선택된 역할(role)을 담아서 약관 동의 화면으로 이동 + navigation.navigate('TermsAgreement', { role }); + }; + + return { + handleRoleSelect, + }; +} \ No newline at end of file diff --git a/src/screens/auth/Signup/styles/birthDatePicker.styles.ts b/src/screens/auth/Signup/styles/birthDatePicker.styles.ts new file mode 100644 index 0000000..db4ffdf --- /dev/null +++ b/src/screens/auth/Signup/styles/birthDatePicker.styles.ts @@ -0,0 +1,42 @@ +import { StyleSheet } from 'react-native'; + +export const birthDatePickerStyles = StyleSheet.create({ + wrapper: { + width: '100%', + }, + mainInput: { + backgroundColor: '#FAFAFA', + borderWidth: 1, + borderColor: '#DDD', + borderRadius: 8, + paddingHorizontal: 12, + height: 48, + fontSize: 14, + color: '#000', + }, + calendarBox: { + marginTop: 8, + backgroundColor: '#fff', + borderWidth: 1, + borderColor: '#dee2e6', + borderRadius: 12, + padding: 8, + elevation: 5, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + }, + closeBtn: { + marginTop: 6, + backgroundColor: '#f1f3f5', + borderRadius: 6, + paddingVertical: 8, + alignItems: 'center', + }, + closeText: { + color: '#495057', + fontSize: 12, + fontWeight: '600', + }, +}); \ No newline at end of file diff --git a/src/screens/auth/Signup/styles/signup.styles.ts b/src/screens/auth/Signup/styles/signup.styles.ts new file mode 100644 index 0000000..6196b4c --- /dev/null +++ b/src/screens/auth/Signup/styles/signup.styles.ts @@ -0,0 +1,116 @@ +import { StyleSheet, Platform, StatusBar } from 'react-native'; + +const PRIMARY_COLOR = '#FF7F00'; + +export const signupStyles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#FFF', + paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 24 : 60, + }, + scrollContent: { + paddingHorizontal: 20, + paddingTop: 20, + paddingBottom: 40, + }, + + // ---------------------------------------------------- + // 헤더: 로고와 타이틀을 세로로 완벽히 분리하고 간격 확보 + // ---------------------------------------------------- + headerContainer: { + flexDirection: 'column', + alignItems: 'flex-start', + marginBottom: 32, + marginTop: 10, + }, + logoWrapper: { + width: 48, + height: 48, + justifyContent: 'center', + alignItems: 'center', + marginBottom: 10, // 로고와 '회원가입' 글자 사이 여백 + }, + title: { + fontSize: 26, + fontWeight: 'bold', + color: '#111827', + includeFontPadding: false, + textAlign: 'left', + lineHeight: 32, + }, + + inputGroup: { + marginBottom: 16, + }, + label: { + fontSize: 14, + fontWeight: '600', + color: '#333', + marginBottom: 8, + }, + input: { + borderWidth: 1, + borderColor: '#E2E8F0', + borderRadius: 8, + paddingHorizontal: 12, + paddingVertical: 12, + fontSize: 14, + color: '#111', + backgroundColor: '#FFF', + }, + submitButton: { + backgroundColor: PRIMARY_COLOR, + borderRadius: 8, + paddingVertical: 14, + alignItems: 'center', + justifyContent: 'center', + marginTop: 24, + }, + submitButtonDisabled: { + backgroundColor: '#CBD5E1', + }, + submitButtonText: { + color: '#FFF', + fontSize: 16, + fontWeight: 'bold', + }, + + // 기저 질환 선택 3열 정렬 스타일 + diseaseGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + marginTop: 8, + rowGap: 14, + }, + diseaseItem: { + width: '33.33%', + }, + checkboxRow: { + flexDirection: 'row', + alignItems: 'center', + }, + checkbox: { + width: 18, + height: 18, + borderWidth: 1.5, + borderColor: '#CED4DA', + borderRadius: 4, + marginRight: 6, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#FFFFFF', + }, + checkboxActive: { + backgroundColor: PRIMARY_COLOR, + borderColor: PRIMARY_COLOR, + }, + checkmark: { + color: '#FFFFFF', + fontSize: 11, + fontWeight: 'bold', + }, + diseaseText: { + fontSize: 13, + color: '#333333', + }, +}); \ No newline at end of file diff --git a/src/screens/auth/Signup/styles/signupTypeSelect.styles.ts b/src/screens/auth/Signup/styles/signupTypeSelect.styles.ts new file mode 100644 index 0000000..ea93fd8 --- /dev/null +++ b/src/screens/auth/Signup/styles/signupTypeSelect.styles.ts @@ -0,0 +1,74 @@ +import { StyleSheet } from 'react-native'; + +export const signupTypeSelectStyles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#FFFFFF', + }, + // 1. 상단 좌측 로고 영역 + headerContainer: { + paddingHorizontal: 24, + paddingTop: 16, + paddingBottom: 10, + alignItems: 'flex-start', + }, + logoWrapper: { + width: 48, + height: 48, + justifyContent: 'center', + alignItems: 'center', + }, + // 2. 중앙 컨텐츠 전체 (위쪽 정렬로 변경해서 전체를 위로 올림!) + contentContainer: { + flex: 1, + alignItems: 'center', + justifyContent: 'flex-start', // ⭐ center에서 flex-start로 변경 ⭐ + paddingTop: 10, // ⭐ 로고 밑에서의 위치 (원하는 만큼 숫자로 조절 가능) ⭐ + paddingHorizontal: 24, + }, + // 강아지 캐릭터 영역 + characterContainer: { + alignItems: 'center', + justifyContent: 'center', + marginTop: -70, + }, + // 메인 타이틀 문구 + title: { + fontSize: 26, + fontWeight: 'bold', + color: '#111111', + textAlign: 'center', + marginTop: -100, // 강아지 발 바로 밑에 붙이기 + marginBottom: 6, + }, + subtitle: { + fontSize: 26, + fontWeight: 'bold', + color: '#111111', + textAlign: 'center', + marginBottom: 32, + }, + // 3. 하단 역할 선택 버튼 영역 + buttonRow: { + flexDirection: 'row', + justifyContent: 'center', + gap: 16, + width: '100%', + paddingHorizontal: 8, + }, + typeButton: { + flex: 1, + height: 58, + backgroundColor: '#FFFFFF', + borderWidth: 2, + borderColor: '#FF7F00', + borderRadius: 12, + justifyContent: 'center', + alignItems: 'center', + }, + buttonText: { + fontSize: 18, + fontWeight: 'bold', + color: '#111111', + }, +}); \ No newline at end of file diff --git a/src/screens/auth/Signup/types/birthDatePicker.types.ts b/src/screens/auth/Signup/types/birthDatePicker.types.ts new file mode 100644 index 0000000..742fd39 --- /dev/null +++ b/src/screens/auth/Signup/types/birthDatePicker.types.ts @@ -0,0 +1,4 @@ +export interface BirthDatePickerProps { + value: string; + onChange: (date: string) => void; +} \ No newline at end of file diff --git a/src/screens/auth/Signup/types/signup.types.ts b/src/screens/auth/Signup/types/signup.types.ts new file mode 100644 index 0000000..6a64ee1 --- /dev/null +++ b/src/screens/auth/Signup/types/signup.types.ts @@ -0,0 +1,10 @@ +export interface SignupFormState { + name: string; + phone: string; + authCode: string; + password: string; + passwordConfirm: string; + birthDate: string; + address: string; + diseases: string[]; +} \ No newline at end of file diff --git a/src/screens/auth/Signup/types/signupType.types.ts b/src/screens/auth/Signup/types/signupType.types.ts new file mode 100644 index 0000000..09d308f --- /dev/null +++ b/src/screens/auth/Signup/types/signupType.types.ts @@ -0,0 +1,5 @@ +export type UserRole = 'PROTECTOR' | 'WARD'; + +export interface RoleSelectHandler { + onSelectRole: (role: UserRole) => void; +} \ No newline at end of file diff --git a/src/screens/auth/terms/CaringLogo.tsx b/src/screens/auth/terms/CaringLogo.tsx new file mode 100644 index 0000000..4f3f8a4 --- /dev/null +++ b/src/screens/auth/terms/CaringLogo.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { View, StyleSheet } from 'react-native'; +import Svg, { Circle, Path, Rect, G, Defs, Pattern, Use, Filter, FeFlood, FeColorMatrix, FeOffset, FeGaussianBlur, FeComposite, FeBlend, LinearGradient, Stop, Image as SvgImage } from 'react-native-svg'; + +export function CaringLogo() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* 👇 이 부분의 href 속성 안에 본인 이미지 데이터를 넣으시면 됩니다 */} + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + justifyContent: 'center', + marginBottom: 8, + }, +}); \ No newline at end of file diff --git a/src/screens/auth/terms/TermsAgreement.styles.ts b/src/screens/auth/terms/TermsAgreement.styles.ts new file mode 100644 index 0000000..70a8b2f --- /dev/null +++ b/src/screens/auth/terms/TermsAgreement.styles.ts @@ -0,0 +1,127 @@ +import { StyleSheet } from 'react-native'; + +export const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#FFFFFF', + paddingHorizontal: 24, + paddingTop: 60, + }, + headerContainer: { + flexDirection: 'row', + alignItems: 'center', + width: '100%', + marginBottom: 20, + position: 'relative', + height: 48, + }, + titleContainer: { + position: 'absolute', + left: 0, + right: 0, + alignItems: 'center', + zIndex: 1, + }, + logoContainer: { + justifyContent: 'center', + zIndex: 2, + }, + title: { + fontSize: 20, + fontWeight: 'bold', + color: '#000000', + }, + subtitle: { + fontSize: 14, + color: '#666666', + marginBottom: 24, + }, + allAgreeBox: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#F5F6F8', + padding: 16, + borderRadius: 12, + marginBottom: 24, + }, + allAgreeText: { + fontSize: 16, + fontWeight: 'bold', + color: '#000000', + marginLeft: 12, + }, + sectionTitle: { + fontSize: 14, + fontWeight: '600', + color: '#888888', + marginBottom: 12, + marginTop: 12, + }, + sectionHeader: { + fontSize: 14, + fontWeight: 'bold', + color: '#666666', + marginTop: 16, + marginBottom: 8, + paddingHorizontal: 4, + }, + termItem: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 12, + }, + termText: { + fontSize: 15, + color: '#333333', + marginLeft: 12, + }, + required: { + color: '#FF7E00', + }, + requiredText: { + color: '#FF7E00', + }, + checkbox: { + width: 22, + height: 22, + borderRadius: 4, + borderWidth: 1, + borderColor: '#DDDDDD', + backgroundColor: '#FFFFFF', + justifyContent: 'center', + alignItems: 'center', + }, + checkboxChecked: { + borderColor: '#FF7E00', + backgroundColor: '#FFF9F5', + }, + checkedBox: { + borderColor: '#FF7E00', + backgroundColor: '#FFF9F5', + }, + checkmark: { + color: '#FF7E00', + fontSize: 12, + fontWeight: 'bold', + }, + nextButton: { + backgroundColor: '#FF7E00', + height: 56, + borderRadius: 12, + justifyContent: 'center', + alignItems: 'center', + marginTop: 20, + marginBottom: 30, + }, + disabledButton: { + backgroundColor: '#DDDDDD', + }, + nextButtonDisabled: { + backgroundColor: '#DDDDDD', + }, + nextButtonText: { + fontSize: 16, + fontWeight: 'bold', + color: '#FFFFFF', + }, +}); \ No newline at end of file diff --git a/src/screens/auth/terms/TermsAgreementScreen.tsx b/src/screens/auth/terms/TermsAgreementScreen.tsx new file mode 100644 index 0000000..0118c2b --- /dev/null +++ b/src/screens/auth/terms/TermsAgreementScreen.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { View, Text, TouchableOpacity, ScrollView, SafeAreaView } from 'react-native'; +import { useTermsAgreement, TERM_LIST } from './useTermsAgreement'; +import { styles } from './TermsAgreement.styles'; +import LogoIcon from '@assets/icons/header/caring_logo.svg'; + +export default function TermsAgreementScreen({ navigation }: any) { + const { checkedItems, isAllChecked, isRequiredChecked, handleCheckItem, handleCheckAll } = useTermsAgreement(); + + const handleNextPress = () => { + if (isRequiredChecked) { + navigation.navigate('Signup'); + } + }; + + return ( + + + + {/* 상단 헤더 (로고 왼쪽, 타이틀 가운데 정렬 구조) */} + + + + + + 약관 동의 + + + + 서비스 이용을 위해 약관에 동의해 주세요. + + {/* 전체 동의 박스 */} + + + {isAllChecked && } + + 전체 동의 + + + {/* 필수 약관 타이틀 */} + 필수 약관 + + {/* 필수 약관 목록 */} + {TERM_LIST.filter(term => term.required).map(term => ( + handleCheckItem(term.id)} + activeOpacity={0.8} + > + + {checkedItems[term.id] && } + + + {term.title} (필수) + + + ))} + + {/* 선택 약관 타이틀 */} + 선택 약관 + + {/* 선택 약관 목록 */} + {TERM_LIST.filter(term => !term.required).map(term => ( + handleCheckItem(term.id)} + activeOpacity={0.8} + > + + {checkedItems[term.id] && } + + {term.title} + + ))} + + {/* 다음 버튼 */} + + 다음 + + + + ); +} \ No newline at end of file diff --git a/src/screens/auth/terms/types.ts b/src/screens/auth/terms/types.ts new file mode 100644 index 0000000..092f793 --- /dev/null +++ b/src/screens/auth/terms/types.ts @@ -0,0 +1,5 @@ +export interface TermItem { + id: string; + title: string; + required: boolean; +} \ No newline at end of file diff --git a/src/screens/auth/terms/useTermsAgreement.ts b/src/screens/auth/terms/useTermsAgreement.ts new file mode 100644 index 0000000..59ecc77 --- /dev/null +++ b/src/screens/auth/terms/useTermsAgreement.ts @@ -0,0 +1,45 @@ +import { useState } from 'react'; +import { TermItem } from './types'; + +export const TERM_LIST: TermItem[] = [ + { id: 'service', title: '서비스 이용약관 동의', required: true }, + { id: 'privacy', title: '개인정보 수집 및 이용 동의', required: true }, + { id: 'location', title: '위치기반 서비스 이용약관 동의', required: true }, + { id: 'thirdParty', title: '개인정보 제3자 제공 동의', required: true }, + { id: 'marketing', title: '마케팅 정보 수신 동의', required: false }, + { id: 'optionalPrivacy', title: '개인정보 선택 수집 동의', required: false }, + { id: 'voice', title: '음성 데이터 수집 및 이용 동의', required: false }, +]; + +export const useTermsAgreement = () => { + const [checkedItems, setCheckedItems] = useState<{ [key: string]: boolean }>({}); + + const isAllChecked = TERM_LIST.every((item) => checkedItems[item.id]); + const isRequiredChecked = TERM_LIST + .filter((item) => item.required) + .every((item) => checkedItems[item.id]); + + const handleCheckItem = (id: string) => { + setCheckedItems((prev) => ({ + ...prev, + [id]: !prev[id], + })); + }; + + const handleCheckAll = () => { + const nextValue = !isAllChecked; + const newCheckedItems: { [key: string]: boolean } = {}; + TERM_LIST.forEach((item) => { + newCheckedItems[item.id] = nextValue; + }); + setCheckedItems(newCheckedItems); + }; + + return { + checkedItems, + isAllChecked, + isRequiredChecked, + handleCheckItem, + handleCheckAll, + }; +}; \ No newline at end of file diff --git a/src/screens/senior/Schedule/SeniorScheduleScreen.tsx b/src/screens/senior/Schedule/SeniorScheduleScreen.tsx new file mode 100644 index 0000000..a4b2456 --- /dev/null +++ b/src/screens/senior/Schedule/SeniorScheduleScreen.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { SafeAreaView, Text, View } from 'react-native'; + +export function SeniorScheduleScreen() { + return ( + + + 오늘 일정 + + + ); +} \ No newline at end of file