Skip to content

Commit 74614bb

Browse files
authored
Merge pull request #146 from JECT-Study/feat/IN-301
feat(IN-301): 마이페이지 계정 탈퇴 페이지 및 탈퇴 플로우 구현
2 parents ee86694 + 3229ecc commit 74614bb

27 files changed

Lines changed: 1158 additions & 21 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { MyAccountDeletePage as default } from '@/pages/me/account-delete'

src/features/me/api/userApi.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { axiosInstance } from '@/shared/api'
2+
3+
import type { WithdrawalReason } from '../model/withdrawalReason'
4+
5+
export interface DeleteUserPayload {
6+
reason: WithdrawalReason
7+
detail?: string
8+
}
9+
10+
export const deleteUser = (payload: DeleteUserPayload) =>
11+
axiosInstance.delete('/user/delete', { data: payload })

src/features/me/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
export { MyPageSidebar } from './ui/MyPageSidebar'
2+
export { useDeleteUser } from './model/useDeleteUser'
3+
export {
4+
WITHDRAWAL_REASONS,
5+
WITHDRAWAL_REASON_LABELS,
6+
} from './model/withdrawalReason'
7+
export type { WithdrawalReason } from './model/withdrawalReason'
28
export { useMyProfile } from './profile/model/useMyProfile'
39
export { useEditPreferencesModal } from './profile/model/useEditPreferencesModal'
410
export { EditPreferencesModal } from './profile/ui/EditPreferencesModal'
5-
export type { MyProfileDto, MyProfileAccountDto, MyProfilePreferencesDto, UserRole, Need } from './profile/types'
11+
export type {
12+
MyProfileDto,
13+
MyProfileAccountDto,
14+
MyProfilePreferencesDto,
15+
UserRole,
16+
Need,
17+
} from './profile/types'
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { useMutation } from '@tanstack/react-query'
2+
3+
import { deleteUser } from '../api/userApi'
4+
5+
export const useDeleteUser = () =>
6+
useMutation({
7+
mutationFn: deleteUser,
8+
})
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export const WITHDRAWAL_REASONS = [
2+
'YOUTUBE_LINK_INCONVENIENT',
3+
'INSUFFICIENT_ANALYSIS_FEATURES',
4+
'EXPENSIVE_PLAN',
5+
'DASHBOARD_DIFFICULT',
6+
'SWITCHING_TO_OTHER_TOOL',
7+
'TEMPORARY_USE',
8+
'OTHER',
9+
] as const
10+
11+
export type WithdrawalReason = (typeof WITHDRAWAL_REASONS)[number]
12+
13+
/* dropdown 표시용 한국어 라벨 — OTHER는 자유 입력 모드 진입 옵션 */
14+
export const WITHDRAWAL_REASON_LABELS: Record<WithdrawalReason, string> = {
15+
YOUTUBE_LINK_INCONVENIENT: '유튜브 채널 연동이 불편해요',
16+
INSUFFICIENT_ANALYSIS_FEATURES: '필요한 분석 기능이 부족해요',
17+
EXPENSIVE_PLAN: '구독 플랜 가격이 부담돼요',
18+
DASHBOARD_DIFFICULT: '대시보드 사용이 어려워요',
19+
SWITCHING_TO_OTHER_TOOL: '다른 마케팅/채널 분석 툴로 이동해요',
20+
TEMPORARY_USE: '일시적으로 사용',
21+
OTHER: '기타',
22+
}

src/features/me/ui/MyPageSidebar.tsx

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,26 @@ export function MyPageSidebar() {
2121
<div className='h-fit w-[30.3rem] shrink-0 gap-20 p-24'>
2222
<div className='flex h-fit w-full gap-4 rounded-8 bg-white p-16 shadow-[0px_2px_6px_0px_#0D0D0D0A]'>
2323
<ul className='flex h-fit w-full flex-col'>
24-
{SIDEBAR_ITEMS.map((item) => (
25-
<li key={item.href}>
26-
<Link
27-
href={item.href}
28-
className={cn(
29-
'flex h-fit w-full items-center gap-8 rounded-6 p-8',
30-
currentPath === item.href
31-
? 'bg-[#5A44F214] text-noto-label-md-bold text-brand-primary'
32-
: 'bg-white text-noto-label-md-thin text-text-and-icon-primary'
33-
)}>
34-
<item.Icon className='size-[1.8rem]' />
35-
{item.label}
36-
</Link>
37-
</li>
38-
))}
24+
{SIDEBAR_ITEMS.map((item) => {
25+
const isActive =
26+
currentPath === item.href ||
27+
(currentPath?.startsWith(item.href + '/') ?? false)
28+
return (
29+
<li key={item.href}>
30+
<Link
31+
href={item.href}
32+
className={cn(
33+
'flex h-fit w-full items-center gap-8 rounded-6 p-8',
34+
isActive
35+
? 'bg-[#5A44F214] text-noto-label-md-bold text-brand-primary'
36+
: 'bg-white text-noto-label-md-thin text-text-and-icon-primary'
37+
)}>
38+
<item.Icon className='size-[1.8rem]' />
39+
{item.label}
40+
</Link>
41+
</li>
42+
)
43+
})}
3944
</ul>
4045
</div>
4146
</div>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { MyAccountDeletePage } from './ui/MyAccountDeletePage'
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
2+
3+
import { MyAccountDeletePage } from './MyAccountDeletePage'
4+
5+
const meta = {
6+
title: 'Pages/Me/MyAccountDeletePage',
7+
component: MyAccountDeletePage,
8+
tags: ['autodocs'],
9+
parameters: {
10+
layout: 'fullscreen',
11+
docs: {
12+
description: {
13+
component:
14+
'계정 탈퇴 페이지 (라우트: `/me/profile/account-delete`). 안내 카드 → 연동 채널 카드 → "계정 탈퇴하기" 버튼 → 모달 3단계 시퀀스. 모달 트리거 후 흐름은 페이지 안에서 직접 확인 가능합니다.',
15+
},
16+
},
17+
},
18+
decorators: [
19+
(Story) => (
20+
<div className='min-h-screen bg-background-gray-default'>
21+
<Story />
22+
</div>
23+
),
24+
],
25+
} satisfies Meta<typeof MyAccountDeletePage>
26+
27+
export default meta
28+
type Story = StoryObj<typeof meta>
29+
30+
export const Default: Story = {}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { useRouter } from 'next/navigation'
5+
6+
import { Button } from '@/shared/ui/button'
7+
import IconArrowLeft from '@/shared/assets/leftwards-arrow-bold.svg'
8+
import {
9+
AccountDeleteModalFlow,
10+
AccountDeleteNoticeCard,
11+
LinkedChannelsForDeleteCard,
12+
} from '@/widgets/me/accountDelete'
13+
import { useAuth } from '@/features/auth'
14+
import { useDeleteUser, type WithdrawalReason } from '@/features/me'
15+
16+
export function MyAccountDeletePage() {
17+
const router = useRouter()
18+
const [open, setOpen] = useState(false)
19+
const { logout } = useAuth()
20+
const { mutateAsync: deleteUser } = useDeleteUser()
21+
22+
async function handleSubmitReason(
23+
reason: WithdrawalReason,
24+
customReason?: string
25+
) {
26+
await deleteUser({
27+
reason,
28+
detail: reason === 'OTHER' ? customReason : undefined,
29+
})
30+
}
31+
32+
/* 탈퇴 완료 — 클라이언트 인증 상태 초기화 + refreshToken 쿠키 삭제 후 홈으로 이동 */
33+
async function handleComplete() {
34+
setOpen(false)
35+
await logout()
36+
}
37+
38+
return (
39+
<div className='flex h-fit w-full flex-col gap-24 p-24'>
40+
<button
41+
type='button'
42+
onClick={() => router.back()}
43+
className='flex w-fit cursor-pointer items-center gap-8 text-ibm-title-lg-normal text-text-and-icon-default'>
44+
<IconArrowLeft className='size-24' />
45+
계정 탈퇴
46+
</button>
47+
48+
<AccountDeleteNoticeCard />
49+
<LinkedChannelsForDeleteCard />
50+
51+
<div className='flex justify-end'>
52+
<Button
53+
type='button'
54+
color='gray'
55+
variant='filled'
56+
size='sm'
57+
onClick={() => setOpen(true)}>
58+
계정 탈퇴하기
59+
</Button>
60+
</div>
61+
62+
<AccountDeleteModalFlow
63+
open={open}
64+
onClose={() => setOpen(false)}
65+
onComplete={handleComplete}
66+
onSubmitReason={handleSubmitReason}
67+
/>
68+
</div>
69+
)
70+
}

src/pages/me/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { MyPageLayout } from './ui/MyPageLayout'
2+
export { MyAccountDeletePage } from './account-delete'

0 commit comments

Comments
 (0)