Skip to content

Commit be454b2

Browse files
authored
Merge pull request #165 from wafflestudio/164-feature-delete-account
회원 탈퇴 기능 추가
2 parents f3b797a + 55d7b9b commit be454b2

File tree

7 files changed

+188
-0
lines changed

7 files changed

+188
-0
lines changed

src/assets/Meta-Logo.png

97.7 KB
Loading
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { z } from 'zod'
2+
import { instance } from '@/shared/api/ky'
3+
4+
const DeleteAccountResponseSchema = z.object({
5+
code: z.string(),
6+
message: z.string(),
7+
data: z.string(),
8+
isSuccess: z.boolean(),
9+
})
10+
11+
export async function deleteAccount(): Promise<string> {
12+
const response = await instance.delete('api/v1/users/me')
13+
const raw = await response.json()
14+
const parsed = DeleteAccountResponseSchema.parse(raw)
15+
return parsed.data
16+
}

src/features/edit-profile/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export { ProfileEditCard } from './ui/ProfileEditCard'
33
export { BioTextarea } from './ui/BioTextarea'
44
export { WebsiteField } from './ui/WebsiteField'
55
export { ChangePhotoModal } from './ui/ChangePhotoModal'
6+
export { MetaAccountCenter } from './ui/MetaAccountCenter'
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {
2+
Dialog,
3+
DialogContent,
4+
DialogDescription,
5+
DialogHeader,
6+
DialogTitle,
7+
} from '@/shared/ui/dialog'
8+
9+
type DeleteAccountModalProps = {
10+
open: boolean
11+
onOpenChange: (open: boolean) => void
12+
onConfirm: () => void
13+
}
14+
15+
export function DeleteAccountModal({
16+
open,
17+
onOpenChange,
18+
onConfirm,
19+
}: DeleteAccountModalProps) {
20+
return (
21+
<Dialog open={open} onOpenChange={onOpenChange}>
22+
<DialogContent
23+
className="max-w-md gap-0 rounded-2xl bg-white p-0"
24+
showCloseButton={false}
25+
>
26+
<DialogHeader className="border-b border-gray-200 p-6">
27+
<DialogTitle className="text-center text-xl font-semibold">
28+
회원 탈퇴
29+
</DialogTitle>
30+
<DialogDescription className="pt-2 text-center text-sm text-gray-500">
31+
탈퇴하면 게시물, 댓글, 좋아요 등 모든 데이터가 삭제되며 복구할 수
32+
없습니다. 정말 탈퇴하시겠어요?
33+
</DialogDescription>
34+
</DialogHeader>
35+
36+
<button
37+
type="button"
38+
className="w-full cursor-pointer border-b border-gray-200 py-4 text-center text-base font-semibold text-red-500"
39+
onClick={() => {
40+
onConfirm()
41+
onOpenChange(false)
42+
}}
43+
>
44+
탈퇴하기
45+
</button>
46+
47+
<button
48+
type="button"
49+
className="w-full cursor-pointer py-4 text-center text-base text-gray-900"
50+
onClick={() => onOpenChange(false)}
51+
>
52+
취소
53+
</button>
54+
</DialogContent>
55+
</Dialog>
56+
)
57+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { ShieldCheck, Tv, User } from 'lucide-react'
2+
import MetaLogo from '@/assets/Meta-Logo.png'
3+
4+
const menuItems = [
5+
{ icon: User, label: '개인정보' },
6+
{ icon: ShieldCheck, label: '비밀번호 및 보안' },
7+
{ icon: Tv, label: '광고 기본 설정' },
8+
]
9+
10+
export function MetaAccountCenter() {
11+
return (
12+
<button
13+
type="button"
14+
className="w-full cursor-pointer rounded-2xl bg-white p-6 text-left shadow-[0_2px_16px_rgba(0,0,0,0.06)]"
15+
>
16+
<img src={MetaLogo} alt="Meta" className="mb-4 h-6" />
17+
18+
<h2 className="mb-2 text-lg font-bold text-gray-900">계정 센터</h2>
19+
<p className="mb-4 text-sm text-gray-500">
20+
Meta 테크놀로지 전반에서 연결된 환경 및 계정 설정을 관리해보세요.
21+
</p>
22+
23+
<ul className="mb-4 flex flex-col gap-3">
24+
{menuItems.map((item) => (
25+
<li
26+
key={item.label}
27+
className="flex items-center gap-3 text-sm text-gray-900"
28+
>
29+
<item.icon className="size-5 text-gray-600" />
30+
{item.label}
31+
</li>
32+
))}
33+
</ul>
34+
35+
<span className="text-sm font-medium text-blue-500">
36+
계정 센터에서 더 보기
37+
</span>
38+
</button>
39+
)
40+
}

src/features/edit-profile/ui/ProfileEditForm.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,27 @@ import {
55
useCurrentUser,
66
useInvalidateCurrentUser,
77
} from '@/shared/auth/useCurrentUser'
8+
import { useAuth } from '@/shared/auth/useAuth'
89
import { cn } from '@/shared/lib/utils'
910
import { uploadImages } from '@/features/create-post/api/uploadImages'
1011
import { updateProfile } from '../api/updateProfile'
12+
import { deleteAccount } from '../api/deleteAccount'
1113
import { ProfileEditCard } from './ProfileEditCard'
1214
import { WebsiteField } from './WebsiteField'
1315
import { BioTextarea } from './BioTextarea'
1416
import { ChangePhotoModal } from './ChangePhotoModal'
17+
import { DeleteAccountModal } from './DeleteAccountModal'
1518

1619
export function ProfileEditForm() {
1720
const { data: currentUser } = useCurrentUser()
1821
const invalidateCurrentUser = useInvalidateCurrentUser()
22+
const { logout } = useAuth()
1923
const [bio, setBio] = useState(currentUser?.bio ?? '')
2024
const [avatarUrl, setAvatarUrl] = useState<string | null>(
2125
currentUser?.profileImageUrl ?? null
2226
)
2327
const [isPhotoModalOpen, setIsPhotoModalOpen] = useState(false)
28+
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
2429
const fileInputRef = useRef<HTMLInputElement>(null)
2530

2631
const originalBio = currentUser?.bio ?? ''
@@ -45,6 +50,17 @@ export function ProfileEditForm() {
4550
},
4651
})
4752

53+
const deleteMutation = useMutation({
54+
mutationFn: deleteAccount,
55+
onSuccess: () => {
56+
toast.success('회원 탈퇴가 완료되었습니다.')
57+
logout()
58+
},
59+
onError: () => {
60+
toast.error('회원 탈퇴에 실패했습니다.')
61+
},
62+
})
63+
4864
const handleSubmit = () => {
4965
updateMutation.mutate({
5066
bio: bio || null,
@@ -89,6 +105,12 @@ export function ProfileEditForm() {
89105
onDelete={handleDeletePhoto}
90106
/>
91107

108+
<DeleteAccountModal
109+
open={isDeleteModalOpen}
110+
onOpenChange={setIsDeleteModalOpen}
111+
onConfirm={() => deleteMutation.mutate()}
112+
/>
113+
92114
<ProfileEditCard
93115
avatarUrl={avatarUrl}
94116
nickname={currentUser.nickname}
@@ -128,6 +150,16 @@ export function ProfileEditForm() {
128150
제출
129151
</button>
130152
</div>
153+
154+
<div className="mt-8 border-t border-gray-200 pt-8">
155+
<button
156+
type="button"
157+
className="cursor-pointer text-sm font-medium text-red-500 hover:text-red-600 hover:underline"
158+
onClick={() => setIsDeleteModalOpen(true)}
159+
>
160+
회원 탈퇴
161+
</button>
162+
</div>
131163
</div>
132164
)
133165
}

src/mocks/handlers/user.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,48 @@ export const userHandlers = [
237237
})
238238
}),
239239

240+
http.delete('*/api/v1/users/me', ({ request }) => {
241+
const userId = getUserIdFromToken(request)
242+
if (!userId) {
243+
return HttpResponse.json(
244+
{
245+
code: 'AUTH_401',
246+
message: '인증이 필요합니다.',
247+
data: null,
248+
isSuccess: false,
249+
},
250+
{ status: 401 }
251+
)
252+
}
253+
254+
const authUserIndex = authDb.findIndex((u) => u.userId === userId)
255+
const profileUserIndex = users.findIndex((u) => u.userId === userId)
256+
257+
if (authUserIndex === -1) {
258+
return HttpResponse.json(
259+
{
260+
code: '404',
261+
message: '사용자를 찾을 수 없습니다.',
262+
data: null,
263+
isSuccess: false,
264+
},
265+
{ status: 404 }
266+
)
267+
}
268+
269+
authDb.splice(authUserIndex, 1)
270+
if (profileUserIndex !== -1) {
271+
users.splice(profileUserIndex, 1)
272+
}
273+
274+
return HttpResponse.json({
275+
code: 'COMMON_200',
276+
message: '요청에 성공하였습니다.',
277+
data: 'Account deleted successfully',
278+
isSuccess: true,
279+
})
280+
}),
281+
240282
http.get('*/api/v1/users/:userId/posts', ({ params }) => {
241283
const userId = Number(params.userId)
242284

0 commit comments

Comments
 (0)