diff --git a/client/src/apis/request/event.ts b/client/src/apis/request/event.ts index f639ba6e..ccbf9f71 100644 --- a/client/src/apis/request/event.ts +++ b/client/src/apis/request/event.ts @@ -45,6 +45,7 @@ export const requestPatchEventName = async ({eventId, eventName}: RequestPatchEv export type RequestPatchUser = Partial; +// TODO: (@soha) 해당 요청은 user.ts 파일로 이동하는 건 어떨지? export const requestPatchUser = async (args: RequestPatchUser) => { return requestPatch({ endpoint: MEMBER_API_PREFIX, diff --git a/client/src/apis/request/user.ts b/client/src/apis/request/user.ts index c1e2f296..db381b5a 100644 --- a/client/src/apis/request/user.ts +++ b/client/src/apis/request/user.ts @@ -1,8 +1,17 @@ import {User} from 'types/serviceType'; import {BASE_URL} from '@apis/baseUrl'; +import {MEMBER_API_PREFIX} from '@apis/endpointPrefix'; +import {requestDelete} from '@apis/fetcher'; import {requestGet} from '@apis/fetcher'; +export const requestDeleteUser = async () => { + await requestDelete({ + baseUrl: BASE_URL.HD, + endpoint: `${MEMBER_API_PREFIX}`, + }); +}; + export const requestGetUserInfo = async () => { return await requestGet({ baseUrl: BASE_URL.HD, diff --git a/client/src/assets/image/check.svg b/client/src/assets/image/check.svg index 66ed7533..09da67bc 100644 --- a/client/src/assets/image/check.svg +++ b/client/src/assets/image/check.svg @@ -1,3 +1,3 @@ - - + + diff --git a/client/src/components/Design/components/Checkbox/Checkbox.stories.tsx b/client/src/components/Design/components/Checkbox/Checkbox.stories.tsx new file mode 100644 index 00000000..a48efe17 --- /dev/null +++ b/client/src/components/Design/components/Checkbox/Checkbox.stories.tsx @@ -0,0 +1,57 @@ +/** @jsxImportSource @emotion/react */ +import type {Meta, StoryObj} from '@storybook/react'; + +import {useEffect, useState} from 'react'; + +import Checkbox from './Checkbox'; + +const meta = { + title: 'Components/Checkbox', + component: Checkbox, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: { + labelText: { + description: '', + control: {type: 'text'}, + }, + isChecked: { + description: '', + control: {type: 'boolean'}, + }, + onChange: { + description: '', + control: {type: 'object'}, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Playground: Story = { + args: { + isChecked: false, + onChange: () => {}, + labelText: '체크박스', + }, + render: ({isChecked, onChange, labelText, ...args}) => { + const [isCheckedState, setIsCheckedState] = useState(isChecked); + const [labelTextState, setLabelTextState] = useState(labelText); + + useEffect(() => { + setIsCheckedState(isChecked); + setLabelTextState(labelText); + }, [isChecked, labelText]); + + const handleToggle = () => { + setIsCheckedState(!isCheckedState); + onChange(); + }; + + return ; + }, +}; diff --git a/client/src/components/Design/components/Checkbox/Checkbox.style.ts b/client/src/components/Design/components/Checkbox/Checkbox.style.ts new file mode 100644 index 00000000..7fe78129 --- /dev/null +++ b/client/src/components/Design/components/Checkbox/Checkbox.style.ts @@ -0,0 +1,38 @@ +import {css} from '@emotion/react'; + +import {WithTheme} from '@components/Design/type/withTheme'; + +interface CheckboxStyleProps { + isChecked: boolean; +} + +export const checkboxStyle = () => + css({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + gap: '0.75rem', + cursor: 'pointer', + }); + +export const inputGroupStyle = ({theme, isChecked}: WithTheme) => + css({ + position: 'relative', + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + + '.check-icon': { + position: 'absolute', + }, + + '.checkbox-input': { + width: '1.375rem', + height: '1.375rem', + border: '1px solid', + borderRadius: '0.5rem', + borderColor: isChecked ? theme.colors.primary : theme.colors.tertiary, + backgroundColor: isChecked ? theme.colors.primary : theme.colors.white, + }, + }); diff --git a/client/src/components/Design/components/Checkbox/Checkbox.tsx b/client/src/components/Design/components/Checkbox/Checkbox.tsx new file mode 100644 index 00000000..6f304880 --- /dev/null +++ b/client/src/components/Design/components/Checkbox/Checkbox.tsx @@ -0,0 +1,28 @@ +/** @jsxImportSource @emotion/react */ +import {useTheme} from '@components/Design/theme/HDesignProvider'; + +import Text from '../Text/Text'; +import Icon from '../Icon/Icon'; + +import {checkboxStyle, inputGroupStyle} from './Checkbox.style'; + +interface Props { + labelText: string; + isChecked: boolean; + onChange: () => void; +} + +const Checkbox = ({labelText, isChecked = false, onChange}: Props) => { + const {theme} = useTheme(); + return ( +
+ {isChecked ? : null} + +
+ {labelText} + + ); +}; + +export default Checkbox; diff --git a/client/src/components/Design/components/Icon/Icon.style.ts b/client/src/components/Design/components/Icon/Icon.style.ts index 4b70b918..1b0443b6 100644 --- a/client/src/components/Design/components/Icon/Icon.style.ts +++ b/client/src/components/Design/components/Icon/Icon.style.ts @@ -37,7 +37,11 @@ export const iconStyle = ({iconType, theme, iconColor}: IconStylePropsWithTheme) const getIconColor = ({iconType, theme, iconColor}: IconStylePropsWithTheme) => { if (iconColor) { return css({ - color: theme.colors[iconColor as ColorKeys], + svg: { + path: { + stroke: theme.colors[iconColor as ColorKeys], + }, + }, }); } else { return css({color: theme.colors[ICON_DEFAULT_COLOR[iconType]]}); diff --git a/client/src/components/Design/components/Textarea/Textarea.stories.tsx b/client/src/components/Design/components/Textarea/Textarea.stories.tsx new file mode 100644 index 00000000..ffbf58c4 --- /dev/null +++ b/client/src/components/Design/components/Textarea/Textarea.stories.tsx @@ -0,0 +1,67 @@ +/** @jsxImportSource @emotion/react */ +import type {Meta, StoryObj} from '@storybook/react'; + +import {useEffect, useState} from 'react'; + +import Textarea from './Textarea'; + +const meta = { + title: 'Components/Textarea', + component: Textarea, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: { + placeholder: { + description: '', + control: {type: 'text'}, + }, + maxLength: { + description: '', + control: {type: 'number'}, + }, + + value: { + description: '', + control: {type: 'text'}, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Playground: Story = { + args: { + placeholder: '내용을 입력해주세요.', + maxLength: 100, + value: '', + }, + render: ({placeholder, value, maxLength, ...args}) => { + const [placeholderState, setPlaceholderState] = useState(placeholder); + const [maxLengthState, setMaxLengthState] = useState(maxLength); + const [valueState, setValueState] = useState(value); + + useEffect(() => { + setPlaceholderState(placeholder); + setMaxLengthState(maxLength); + setValueState(value); + }, [maxLength, placeholder, value]); + + const handleChange = (event: React.ChangeEvent) => { + setValueState(event.target.value); + }; + + return ( + + ); +}); +export default Textarea; diff --git a/client/src/components/Design/components/Textarea/Textarea.type.ts b/client/src/components/Design/components/Textarea/Textarea.type.ts new file mode 100644 index 00000000..43c4a7b3 --- /dev/null +++ b/client/src/components/Design/components/Textarea/Textarea.type.ts @@ -0,0 +1,13 @@ +export interface TextareaStyleProps { + height?: string; +} + +export interface TextareaCustomProps { + value: string; + maxLength?: number; + placeholder?: string; +} + +export type TextareaOptionProps = TextareaStyleProps & TextareaCustomProps; + +export type TextareaProps = React.ComponentProps<'textarea'> & TextareaOptionProps; diff --git a/client/src/components/Design/index.tsx b/client/src/components/Design/index.tsx index ca69f1a1..9fc57cd8 100644 --- a/client/src/components/Design/index.tsx +++ b/client/src/components/Design/index.tsx @@ -24,6 +24,8 @@ import DepositToggle from './components/DepositToggle/DepositToggle'; import Amount from './components/Amount/Amount'; import Dropdown from './components/Dropdown/Dropdown'; import DropdownButton from './components/Dropdown/DropdownButton'; +import Checkbox from './components/Checkbox/Checkbox'; +import Textarea from './components/Textarea/Textarea'; import {Select} from './components/Select'; export { @@ -55,4 +57,6 @@ export { DropdownButton, useTheme, Select, + Checkbox, + Textarea, }; diff --git a/client/src/components/Logo/StandingDogLogo.tsx b/client/src/components/Logo/StandingDogLogo.tsx index cb6a1350..ea69fa83 100644 --- a/client/src/components/Logo/StandingDogLogo.tsx +++ b/client/src/components/Logo/StandingDogLogo.tsx @@ -2,12 +2,16 @@ import Image from '@components/Design/components/Image/Image'; import getImageUrl from '@utils/getImageUrl'; -import {logoStyle} from './Logo.style'; +import {logoStyle, logoImageStyle} from './Logo.style'; const StandingDogLogo = () => { return (
- +
); }; diff --git a/client/src/constants/routerUrls.ts b/client/src/constants/routerUrls.ts index 2b4d6f87..e3a3f49b 100644 --- a/client/src/constants/routerUrls.ts +++ b/client/src/constants/routerUrls.ts @@ -18,6 +18,7 @@ export const ROUTER_URLS = { event: EVENT, login: '/login', myPage: '/mypage', + withdraw: '/mypage/withdraw', createdEvents: '/mypage/events', guestEventLogin: `${EVENT_WITH_EVENT_ID}/admin/guest/login`, memberEventLogin: `${EVENT_WITH_EVENT_ID}/admin/member/login`, diff --git a/client/src/hooks/queries/user/useRequestDeleteUser.ts b/client/src/hooks/queries/user/useRequestDeleteUser.ts new file mode 100644 index 00000000..b232c284 --- /dev/null +++ b/client/src/hooks/queries/user/useRequestDeleteUser.ts @@ -0,0 +1,21 @@ +import {useMutation, useQueryClient} from '@tanstack/react-query'; + +import {requestDeleteUser} from '@apis/request/user'; + +import QUERY_KEYS from '@constants/queryKeys'; + +const useRequestDeleteUser = () => { + const queryClient = useQueryClient(); + + const {mutateAsync, ...rest} = useMutation({ + mutationFn: () => requestDeleteUser(), + onSuccess: () => { + queryClient.invalidateQueries({queryKey: [QUERY_KEYS.kakaoLogin]}); + queryClient.invalidateQueries({queryKey: [QUERY_KEYS.kakaoClientId]}); + }, + }); + + return {deleteAsyncUser: mutateAsync, ...rest}; +}; + +export default useRequestDeleteUser; diff --git a/client/src/hooks/useWithdrawFunnel.ts b/client/src/hooks/useWithdrawFunnel.ts new file mode 100644 index 00000000..79bcac6d --- /dev/null +++ b/client/src/hooks/useWithdrawFunnel.ts @@ -0,0 +1,28 @@ +import {useEffect, useState} from 'react'; + +export type WithdrawStep = + | 'withdrawReason' + | 'notUseService' + | 'unableToUseDueToError' + | 'cantFigureOutHowToUseIt' + | 'etc' + | 'checkBeforeWithdrawing' + | 'withdrawalCompleted'; + +const useWithdrawFunnel = () => { + const [step, setStep] = useState('withdrawReason'); + + useEffect(() => { + document.body.style.overflow = 'hidden'; + + return () => { + document.body.style.overflow = 'auto'; + }; + }, []); + + const handleMoveStep = (nextStep: WithdrawStep) => setStep(nextStep); + + return {step, handleMoveStep}; +}; + +export default useWithdrawFunnel; diff --git a/client/src/pages/MyPage/WithdrawPage/WithdrawPage.tsx b/client/src/pages/MyPage/WithdrawPage/WithdrawPage.tsx new file mode 100644 index 00000000..7530a815 --- /dev/null +++ b/client/src/pages/MyPage/WithdrawPage/WithdrawPage.tsx @@ -0,0 +1,30 @@ +import useWithdrawFunnel from '@hooks/useWithdrawFunnel'; + +import {MainLayout, TopNav} from '@components/Design'; + +import ReasonStep from './steps/ReasonStep'; +import NotUseServiceStep from './steps/NotUseServiceStep'; +import EtcStep from './steps/EtcStep'; +import CheckBeforeWithdrawingStep from './steps/CheckBeforeWithdrawingStep'; +import WithdrawalCompleted from './steps/WithdrawalCompleted'; +import UnableToUseDueToError from './steps/UnableToUseDueToError'; + +const WithdrawPage = () => { + const {step, handleMoveStep} = useWithdrawFunnel(); + + return ( + + + + + {step === 'withdrawReason' && } + {step === 'notUseService' && } + {step === 'unableToUseDueToError' && } + {step === 'etc' && } + {step === 'checkBeforeWithdrawing' && } + {step === 'withdrawalCompleted' && } + + ); +}; + +export default WithdrawPage; diff --git a/client/src/pages/MyPage/WithdrawPage/steps/CheckBeforeWithdrawingStep.tsx b/client/src/pages/MyPage/WithdrawPage/steps/CheckBeforeWithdrawingStep.tsx new file mode 100644 index 00000000..64948e6c --- /dev/null +++ b/client/src/pages/MyPage/WithdrawPage/steps/CheckBeforeWithdrawingStep.tsx @@ -0,0 +1,51 @@ +import {css} from '@emotion/react'; + +import StandingDogLogo from '@components/Logo/StandingDogLogo'; +import useRequestDeleteUser from '@hooks/queries/user/useRequestDeleteUser'; +import toast from '@hooks/useToast/toast'; + +import {WithdrawStep} from '@hooks/useWithdrawFunnel'; + +import {Top, FixedButton, Flex, Text} from '@components/Design'; + +const CheckBeforeWithdrawingStep = ({handleMoveStep}: {handleMoveStep: (nextStep: WithdrawStep) => void}) => { + const {deleteAsyncUser} = useRequestDeleteUser(); + + const handleWithdraw = async () => { + try { + await deleteAsyncUser(); + handleMoveStep('withdrawalCompleted'); + } catch (error) { + toast.error('회원 탈퇴에 실패했어요.', { + showingTime: 3000, + position: 'bottom', + }); + } + }; + return ( + <> +
+ + + + + + • 행동대장에서 관리했던 __님의 모든 개인정보를 다시 볼 수 없어요. + • 지난 행사 목록이 모두 사라져요. + • 개인 정보는 즉시 파기돼요. + + +
+ 탈퇴하기 + + ); +}; + +export default CheckBeforeWithdrawingStep; diff --git a/client/src/pages/MyPage/WithdrawPage/steps/EtcStep.tsx b/client/src/pages/MyPage/WithdrawPage/steps/EtcStep.tsx new file mode 100644 index 00000000..7689d8fd --- /dev/null +++ b/client/src/pages/MyPage/WithdrawPage/steps/EtcStep.tsx @@ -0,0 +1,38 @@ +import {css} from '@emotion/react'; + +import {WithdrawStep} from '@hooks/useWithdrawFunnel'; + +import {Top, Textarea, FixedButton, Flex} from '@components/Design'; + +const EtcStep = ({handleMoveStep}: {handleMoveStep: (nextStep: WithdrawStep) => void}) => { + return ( + <> +
+ + + + + + +