From 557f77b3cbfa8ad281faffe187884f4d54f8f0eb Mon Sep 17 00:00:00 2001 From: Sohyun Kim <79398566+sohyun215@users.noreply.github.com> Date: Mon, 7 Oct 2024 23:28:40 +0900 Subject: [PATCH] =?UTF-8?q?Feat/#40=20=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=A9=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 디자인 시스템 색상을 수정합니다. * feat: Toggle 컴포넌트를 추가합니다. * feat: mypage 화면을 추가합니다. * feat: 토글 배경 색상을 변경합니다. * feat: 직군 선택 페이지를 추가합니다. * fix: css prop에 $를 붙이도록 수정합니다. * refactor: 불필요한 render 코드를 삭제합니다. --- app/(app)/my/_layout.tsx | 50 ++++++- app/(app)/my/cancel-account.tsx | 81 ++++++++++- app/(app)/my/index.tsx | 17 ++- app/(app)/my/job.tsx | 130 ++++++++++++++++++ app/(app)/my/policy.tsx | 50 +++++++ app/(app)/my/profile.tsx | 13 -- .../common/toggle/Toggle.stories.tsx | 51 +++++++ src/components/common/toggle/index.tsx | 63 +++++++++ src/components/mypage/MenuList.style.ts | 16 +++ src/components/mypage/MenuList.tsx | 85 ++++++++++++ src/components/mypage/Profile.style.ts | 46 +++++++ src/components/mypage/Profile.tsx | 40 ++++++ src/constants/navigations.ts | 7 +- 13 files changed, 621 insertions(+), 28 deletions(-) create mode 100644 app/(app)/my/job.tsx create mode 100644 app/(app)/my/policy.tsx delete mode 100644 app/(app)/my/profile.tsx create mode 100644 src/components/common/toggle/Toggle.stories.tsx create mode 100644 src/components/common/toggle/index.tsx create mode 100644 src/components/mypage/MenuList.style.ts create mode 100644 src/components/mypage/MenuList.tsx create mode 100644 src/components/mypage/Profile.style.ts create mode 100644 src/components/mypage/Profile.tsx diff --git a/app/(app)/my/_layout.tsx b/app/(app)/my/_layout.tsx index 49b7ed7..3b310f3 100644 --- a/app/(app)/my/_layout.tsx +++ b/app/(app)/my/_layout.tsx @@ -1,12 +1,56 @@ -import { Stack } from 'expo-router'; +import { Feather } from '@expo/vector-icons'; +import { router, Stack } from 'expo-router'; +import { Pressable } from 'react-native'; + +import { MY_NAVIGATIONS } from '@/constants'; +import { color } from '@/styles/theme'; +import { isMobile } from '@/utils'; function Layout() { return ( - + ({ + title: '마이페이지', + headerStyle: { height: 40, backgroundColor: color.Background.Alternative }, + headerTitleStyle: { + paddingTop: 8, + paddingBottom: 6, + fontFamily: 'Pretendard-SemiBold', + }, + headerTitleAlign: 'center', + headerShadowVisible: false, + headerLeft: ({ canGoBack }) => ( + (canGoBack ? router.back() : router.push(MY_NAVIGATIONS.HOME))} + style={{ + paddingLeft: isMobile ? 0 : 20, + width: 24, + height: 24, + }}> + + + ), + })}> null }} + /> + + + ); } diff --git a/app/(app)/my/cancel-account.tsx b/app/(app)/my/cancel-account.tsx index 2169e42..90fdc60 100644 --- a/app/(app)/my/cancel-account.tsx +++ b/app/(app)/my/cancel-account.tsx @@ -1,13 +1,86 @@ -import { View } from 'react-native'; +import styled from '@emotion/native'; +import SolidButton from '@/components/common/button/SolidButton'; import Typography from '@/components/common/typography'; +import { useTabBarEffect } from '@/hooks'; +import { flexDirectionColumnCenter, flexDirectionRow } from '@/styles/common'; +import { theme } from '@/styles/theme'; function CancelAccount() { + useTabBarEffect(); + return ( - - cancel-account - + + + 정말 위프로를{'\n'}떠나실건가요? + + + 계정 탈퇴 신청 전 아래 사항을 확인 부탁드립니다. + + + 1. + + 탈퇴 후 15일까지 재로그인을 통해 철회가 가능하며 이후에는 모든 회원 정보가 지체 없이 + 파기됩니다. + + + + 2. + + 휴대폰 인증을 통해 생성한 아이디가 여러 개인 경우 1개의 아이디를 탈퇴해도 다른 아이디는 + 계속해서 사용 가능합니다. + + + + + + 계속 이용하기 + + + 탈퇴하기 + + + ); } export default CancelAccount; + +const S = { + Container: styled.View` + flex: 1; + padding: 23px 20px 0; + color: ${({ theme }) => theme.color.Label.Normal}; + background-color: ${({ theme }) => theme.color.Background.Alternative}; + `, + Number: styled(Typography)` + flex-shrink: 0; + width: 20px; + height: 20px; + `, + DescriptionContainer: styled.View` + ${flexDirectionRow}; + align-items: flex-start; + `, + DescriptionText: styled(Typography)` + flex-shrink: 1; + flex-wrap: wrap; + `, + ButtonSection: styled.View` + ${flexDirectionColumnCenter} + position: absolute; + right: 0; + bottom: 0; + left: 0; + gap: 8px; + padding: 12px 20px 30px; + `, +}; diff --git a/app/(app)/my/index.tsx b/app/(app)/my/index.tsx index cf240d4..90d8f33 100644 --- a/app/(app)/my/index.tsx +++ b/app/(app)/my/index.tsx @@ -1,12 +1,19 @@ -import { View } from 'react-native'; +import { SafeAreaView } from 'react-native'; -import Typography from '@/components/common/typography'; +import MenuList from '@/components/mypage/MenuList'; +import Profile from '@/components/mypage/Profile'; +import { color } from '@/styles/theme'; function My() { return ( - - :) - + + + + ); } diff --git a/app/(app)/my/job.tsx b/app/(app)/my/job.tsx new file mode 100644 index 0000000..ef56b54 --- /dev/null +++ b/app/(app)/my/job.tsx @@ -0,0 +1,130 @@ +import styled from '@emotion/native'; +import { router } from 'expo-router'; +import { useState } from 'react'; +import { Platform } from 'react-native'; + +import SolidButton from '@/components/common/button/SolidButton'; +import Typography from '@/components/common/typography'; +import { useTabBarEffect } from '@/hooks'; +import { flexDirectionColumnCenter } from '@/styles/common'; +import { theme } from '@/styles/theme'; + +const JOB_LIST = ['개발자', '디자이너', 'PM', '그 외']; + +function JOB() { + useTabBarEffect(); + + const [selectedJob, setSelectedJob] = useState(null); + const [otherJob, setOtherJob] = useState(''); + + const isSelected = (job: string) => selectedJob === job; + + const handleSaveJob = () => { + // TODO: 직군 저장 + console.log(selectedJob === '그 외' ? otherJob : selectedJob); + router.navigate('my'); + }; + + return ( + + + 무슨 일을 하시는지{'\n'}알려주세요 + + + {JOB_LIST.map((job) => + job === '그 외' && isSelected(job) ? ( + + + + ) : ( + setSelectedJob(job)} + $selected={isSelected(job)} + style={[ + Platform.OS === 'ios' && { + shadowColor: isSelected(job) ? 'rgba(26, 117, 255, 0.20)' : 'rgba(0, 0, 0, 0.05)', + shadowOffset: { + width: 0, + height: isSelected(job) ? 0 : 1, + }, + shadowOpacity: 1, + shadowRadius: isSelected(job) ? 6 : 10, + }, + Platform.OS === 'android' && { + elevation: 0.5, + }, + ]}> + + {job} + + + ) + )} + + + + 완료 + + + + ); +} + +export default JOB; + +const S = { + Container: styled.View` + flex: 1; + padding: 30px 20px 0; + background-color: ${({ theme }) => theme.color.Background.Alternative}; + `, + JobList: styled.View` + gap: 12px; + margin-top: 34px; + `, + JobSelectButton: styled.Pressable<{ $selected: boolean }>` + justify-content: center; + height: 64px; + padding: 20px 16px; + background-color: ${({ theme, $selected }) => + $selected ? theme.color.Blue[95] : theme.color.Common[100]}; + border-color: ${({ theme, $selected }) => + $selected ? theme.color.Primary.Normal : theme.color.Line.Neutral}; + border-width: 1px; + border-radius: 8px; + `, + ButtonSection: styled.View` + ${flexDirectionColumnCenter} + position: absolute; + right: 0; + bottom: 0; + left: 0; + padding: 12px 20px 52px; + `, + InputContainer: styled.View` + height: 64px; + padding: 20px 16px; + background-color: ${({ theme }) => theme.color.Blue[95]}; + border-color: ${({ theme }) => theme.color.Primary.Normal}; + border-width: 1px; + border-radius: 8px; + `, + TextInput: styled.TextInput` + flex: 1; + font-size: 16px; + outline-width: 0; + `, +}; diff --git a/app/(app)/my/policy.tsx b/app/(app)/my/policy.tsx new file mode 100644 index 0000000..7e26c6e --- /dev/null +++ b/app/(app)/my/policy.tsx @@ -0,0 +1,50 @@ +import styled from '@emotion/native'; + +import SolidButton from '@/components/common/button/SolidButton'; +import Typography from '@/components/common/typography'; +import { useTabBarEffect } from '@/hooks'; +import { flexDirectionColumnCenter } from '@/styles/common'; +import { theme } from '@/styles/theme'; + +function Policy() { + useTabBarEffect(); + + return ( + + + + 계속 이용하기 + + + 탈퇴하기 + + + + ); +} + +export default Policy; + +const S = { + Container: styled.View` + flex: 1; + padding: 23px 20px 0; + color: ${({ theme }) => theme.color.Label.Normal}; + background-color: ${({ theme }) => theme.color.Background.Alternative}; + `, + ButtonSection: styled.View` + ${flexDirectionColumnCenter} + position: absolute; + right: 0; + bottom: 0; + left: 0; + gap: 8px; + padding: 12px 20px 30px; + `, +}; diff --git a/app/(app)/my/profile.tsx b/app/(app)/my/profile.tsx deleted file mode 100644 index 4f181e5..0000000 --- a/app/(app)/my/profile.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { View } from 'react-native'; - -import Typography from '@/components/common/typography'; - -function Profile() { - return ( - - Profile - - ); -} - -export default Profile; diff --git a/src/components/common/toggle/Toggle.stories.tsx b/src/components/common/toggle/Toggle.stories.tsx new file mode 100644 index 0000000..0b153a5 --- /dev/null +++ b/src/components/common/toggle/Toggle.stories.tsx @@ -0,0 +1,51 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import Toggle from './'; + +const ToggleMeta: Meta = { + title: 'common/Toggle', + component: Toggle, + argTypes: { + isOn: { + control: { + type: 'boolean', + }, + description: '토글 상태를 설정합니다.', + }, + disabled: { + control: { + type: 'boolean', + }, + description: '비활성화 여부를 설정합니다.', + }, + onToggle: { + action: 'clicked', + description: '토글 상태가 변경될 때 호출되는 콜백 함수입니다.', + }, + }, +}; + +export default ToggleMeta; + +export const Default: StoryObj = { + args: { + isOn: true, + disabled: false, + onToggle: () => {}, + }, +}; + +export const Off: StoryObj = { + args: { + isOn: false, + disabled: false, + onToggle: () => {}, + }, +}; + +export const Disabled: StoryObj = { + args: { + disabled: true, + onToggle: () => {}, + }, +}; diff --git a/src/components/common/toggle/index.tsx b/src/components/common/toggle/index.tsx new file mode 100644 index 0000000..d22ca0d --- /dev/null +++ b/src/components/common/toggle/index.tsx @@ -0,0 +1,63 @@ +import styled, { css } from '@emotion/native'; +import { useEffect, useRef } from 'react'; +import { Animated } from 'react-native'; + +import { theme } from '@/styles/theme'; + +interface ToggleProps { + isOn: boolean; + disabled?: boolean; + onToggle: () => void; +} + +function Toggle({ onToggle, isOn = true, disabled = false }: ToggleProps) { + const translateX = useRef(new Animated.Value(isOn && !disabled ? 20 : 0)).current; + + useEffect(() => { + Animated.timing(translateX, { + toValue: isOn && !disabled ? 20 : 0, + duration: 200, + useNativeDriver: true, + }).start(); + }, [isOn, translateX, disabled]); + + const backgroundColor = + disabled || !isOn ? theme.color.Label.Disable : theme.color.Primary.Normal; + + return ( + + + + ); +} + +export default Toggle; + +const S = { + Container: styled.Pressable` + display: flex; + flex-shrink: 0; + gap: 10px; + width: 44px; + height: 24px; + padding: 2px; + border-radius: 20px; + `, + Circle: styled(Animated.View)<{ $isOn: boolean }>` + width: 20px; + height: 20px; + background-color: ${({ theme }) => theme.color.Common[100]}; + ${({ $isOn }) => + $isOn && + css` + filter: drop-shadow(1px 1px 2px rgb(0 0 0 / 10%)); + `} + border-radius: 20px; + `, +}; diff --git a/src/components/mypage/MenuList.style.ts b/src/components/mypage/MenuList.style.ts new file mode 100644 index 0000000..d2a9be1 --- /dev/null +++ b/src/components/mypage/MenuList.style.ts @@ -0,0 +1,16 @@ +import styled from '@emotion/native'; + +import { flexDirectionRowItemsCenter } from '@/styles/common'; + +export const MenuListContainer = styled.View` + padding: 0 20px; +`; + +export const OneMenuContainer = styled.View` + ${flexDirectionRowItemsCenter}; + justify-content: space-between; + padding: 20px 0; + border-color: ${({ theme }) => theme.color.Line.Neutral}; + border-bottom-style: solid; + border-bottom-width: 1px; +`; diff --git a/src/components/mypage/MenuList.tsx b/src/components/mypage/MenuList.tsx new file mode 100644 index 0000000..fe53c2e --- /dev/null +++ b/src/components/mypage/MenuList.tsx @@ -0,0 +1,85 @@ +import { Feather } from '@expo/vector-icons'; +import { router } from 'expo-router'; +import { useState } from 'react'; +import { Pressable } from 'react-native'; + +import { useSession } from '@/store'; +import { theme } from '@/styles/theme'; + +import Toggle from '../common/toggle'; +import Typography from '../common/typography'; +import * as S from './MenuList.style'; + +type OneMenuProps = { + title: string; + isArrowVisible?: boolean; + onPress: () => void; +}; + +function OneMenu({ title, onPress, isArrowVisible = false }: OneMenuProps) { + return ( + + + + {title} + + {isArrowVisible && ( + + )} + + + ); +} + +function MenuList() { + const { signOut } = useSession(); + + // TODO: 알림 설정 기능 추가 + const [isNotificationOn, setIsNotificationOn] = useState(true); + + return ( + + router.push('/my/job')} + /> + + + 알림 설정 + + { + setIsNotificationOn((prev) => !prev); + }} + /> + + router.push('/my/policy')} + /> + { + signOut(); + }} + /> + router.push('/my/cancel-account')} + /> + + ); +} + +export default MenuList; diff --git a/src/components/mypage/Profile.style.ts b/src/components/mypage/Profile.style.ts new file mode 100644 index 0000000..c24c9fa --- /dev/null +++ b/src/components/mypage/Profile.style.ts @@ -0,0 +1,46 @@ +import styled from '@emotion/native'; + +import { flexDirectionColumnItemsCenter } from '@/styles/common'; + +export const ProfileContainer = styled.View` + ${flexDirectionColumnItemsCenter}; + gap: 12px; + padding: 0 20px; + margin-top: 36px; + margin-bottom: 20px; +`; + +export const ProfileImageContainer = styled.View` + ${flexDirectionColumnItemsCenter}; + position: relative; + width: 94px; + height: 94px; + border-radius: 94px; +`; + +export const CameraIcon = styled.Pressable` + position: absolute; + right: -7px; + bottom: 0; + width: 40px; + height: 40px; + padding: 8px; + background-color: ${({ theme }) => theme.color.Background.Alternative}; + border: 1px solid ${({ theme }) => theme.color.Line.Normal}; + border-radius: 22.5px; + ${flexDirectionColumnItemsCenter}; +`; + +export const ProfileImage = styled.Image` + position: relative; + width: 100%; + height: 100%; + border: 1.5px solid ${({ theme }) => theme.color.Line.Normal}; + border-radius: 94px; +`; + +export const ProfileInfo = styled.View` + ${flexDirectionColumnItemsCenter}; + gap: 4px; + padding: 10px 0; +`; diff --git a/src/components/mypage/Profile.tsx b/src/components/mypage/Profile.tsx new file mode 100644 index 0000000..4eff68b --- /dev/null +++ b/src/components/mypage/Profile.tsx @@ -0,0 +1,40 @@ +import { Feather } from '@expo/vector-icons'; + +import { theme } from '@/styles/theme'; + +import Typography from '../common/typography'; +import * as S from './Profile.style'; + +type Props = { + uri: string; + name: string; + job: string; +}; + +function Profile({ uri, name, job }: Props) { + return ( + + + + + + + + + + + {name} + + {job} + + + ); +} + +export default Profile; diff --git a/src/constants/navigations.ts b/src/constants/navigations.ts index 29db173..e4e2279 100644 --- a/src/constants/navigations.ts +++ b/src/constants/navigations.ts @@ -7,9 +7,10 @@ export const MAIN_NAVIGATIONS = { export type MainNavigations = (typeof MAIN_NAVIGATIONS)[keyof typeof MAIN_NAVIGATIONS]; export const MY_NAVIGATIONS = { - HOME: 'index', - CREATE: 'create', - DELETE: 'delete', + HOME: 'my', + JOB: 'job', + POLICY: 'policy', + CANCEL_ACCOUNT: 'cancel-account', }; export const PROJECT_NAVIGATIONS = {