diff --git a/src/assets/Meta-Logo.png b/src/assets/Meta-Logo.png new file mode 100644 index 0000000..af81af3 Binary files /dev/null and b/src/assets/Meta-Logo.png differ diff --git a/src/features/edit-profile/api/deleteAccount.ts b/src/features/edit-profile/api/deleteAccount.ts new file mode 100644 index 0000000..4f0bbb0 --- /dev/null +++ b/src/features/edit-profile/api/deleteAccount.ts @@ -0,0 +1,16 @@ +import { z } from 'zod' +import { instance } from '@/shared/api/ky' + +const DeleteAccountResponseSchema = z.object({ + code: z.string(), + message: z.string(), + data: z.string(), + isSuccess: z.boolean(), +}) + +export async function deleteAccount(): Promise { + const response = await instance.delete('api/v1/users/me') + const raw = await response.json() + const parsed = DeleteAccountResponseSchema.parse(raw) + return parsed.data +} diff --git a/src/features/edit-profile/index.ts b/src/features/edit-profile/index.ts index 6036b11..db731c8 100644 --- a/src/features/edit-profile/index.ts +++ b/src/features/edit-profile/index.ts @@ -3,3 +3,4 @@ export { ProfileEditCard } from './ui/ProfileEditCard' export { BioTextarea } from './ui/BioTextarea' export { WebsiteField } from './ui/WebsiteField' export { ChangePhotoModal } from './ui/ChangePhotoModal' +export { MetaAccountCenter } from './ui/MetaAccountCenter' diff --git a/src/features/edit-profile/ui/DeleteAccountModal.tsx b/src/features/edit-profile/ui/DeleteAccountModal.tsx new file mode 100644 index 0000000..7c6f3d7 --- /dev/null +++ b/src/features/edit-profile/ui/DeleteAccountModal.tsx @@ -0,0 +1,57 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/shared/ui/dialog' + +type DeleteAccountModalProps = { + open: boolean + onOpenChange: (open: boolean) => void + onConfirm: () => void +} + +export function DeleteAccountModal({ + open, + onOpenChange, + onConfirm, +}: DeleteAccountModalProps) { + return ( + + + + + 회원 탈퇴 + + + 탈퇴하면 게시물, 댓글, 좋아요 등 모든 데이터가 삭제되며 복구할 수 + 없습니다. 정말 탈퇴하시겠어요? + + + + + + + + + ) +} diff --git a/src/features/edit-profile/ui/MetaAccountCenter.tsx b/src/features/edit-profile/ui/MetaAccountCenter.tsx new file mode 100644 index 0000000..b868fdd --- /dev/null +++ b/src/features/edit-profile/ui/MetaAccountCenter.tsx @@ -0,0 +1,40 @@ +import { ShieldCheck, Tv, User } from 'lucide-react' +import MetaLogo from '@/assets/Meta-Logo.png' + +const menuItems = [ + { icon: User, label: '개인정보' }, + { icon: ShieldCheck, label: '비밀번호 및 보안' }, + { icon: Tv, label: '광고 기본 설정' }, +] + +export function MetaAccountCenter() { + return ( + + ) +} diff --git a/src/features/edit-profile/ui/ProfileEditForm.tsx b/src/features/edit-profile/ui/ProfileEditForm.tsx index def5f74..c3d1835 100644 --- a/src/features/edit-profile/ui/ProfileEditForm.tsx +++ b/src/features/edit-profile/ui/ProfileEditForm.tsx @@ -5,22 +5,27 @@ import { useCurrentUser, useInvalidateCurrentUser, } from '@/shared/auth/useCurrentUser' +import { useAuth } from '@/shared/auth/useAuth' import { cn } from '@/shared/lib/utils' import { uploadImages } from '@/features/create-post/api/uploadImages' import { updateProfile } from '../api/updateProfile' +import { deleteAccount } from '../api/deleteAccount' import { ProfileEditCard } from './ProfileEditCard' import { WebsiteField } from './WebsiteField' import { BioTextarea } from './BioTextarea' import { ChangePhotoModal } from './ChangePhotoModal' +import { DeleteAccountModal } from './DeleteAccountModal' export function ProfileEditForm() { const { data: currentUser } = useCurrentUser() const invalidateCurrentUser = useInvalidateCurrentUser() + const { logout } = useAuth() const [bio, setBio] = useState(currentUser?.bio ?? '') const [avatarUrl, setAvatarUrl] = useState( currentUser?.profileImageUrl ?? null ) const [isPhotoModalOpen, setIsPhotoModalOpen] = useState(false) + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) const fileInputRef = useRef(null) const originalBio = currentUser?.bio ?? '' @@ -45,6 +50,17 @@ export function ProfileEditForm() { }, }) + const deleteMutation = useMutation({ + mutationFn: deleteAccount, + onSuccess: () => { + toast.success('회원 탈퇴가 완료되었습니다.') + logout() + }, + onError: () => { + toast.error('회원 탈퇴에 실패했습니다.') + }, + }) + const handleSubmit = () => { updateMutation.mutate({ bio: bio || null, @@ -89,6 +105,12 @@ export function ProfileEditForm() { onDelete={handleDeletePhoto} /> + deleteMutation.mutate()} + /> + + +
+ +
) } diff --git a/src/mocks/handlers/user.ts b/src/mocks/handlers/user.ts index 031adfa..c6582a4 100644 --- a/src/mocks/handlers/user.ts +++ b/src/mocks/handlers/user.ts @@ -237,6 +237,48 @@ export const userHandlers = [ }) }), + http.delete('*/api/v1/users/me', ({ request }) => { + const userId = getUserIdFromToken(request) + if (!userId) { + return HttpResponse.json( + { + code: 'AUTH_401', + message: '인증이 필요합니다.', + data: null, + isSuccess: false, + }, + { status: 401 } + ) + } + + const authUserIndex = authDb.findIndex((u) => u.userId === userId) + const profileUserIndex = users.findIndex((u) => u.userId === userId) + + if (authUserIndex === -1) { + return HttpResponse.json( + { + code: '404', + message: '사용자를 찾을 수 없습니다.', + data: null, + isSuccess: false, + }, + { status: 404 } + ) + } + + authDb.splice(authUserIndex, 1) + if (profileUserIndex !== -1) { + users.splice(profileUserIndex, 1) + } + + return HttpResponse.json({ + code: 'COMMON_200', + message: '요청에 성공하였습니다.', + data: 'Account deleted successfully', + isSuccess: true, + }) + }), + http.get('*/api/v1/users/:userId/posts', ({ params }) => { const userId = Number(params.userId)