Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added src/assets/Meta-Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions src/features/edit-profile/api/deleteAccount.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
const response = await instance.delete('api/v1/users/me')
const raw = await response.json()
const parsed = DeleteAccountResponseSchema.parse(raw)
return parsed.data
}
1 change: 1 addition & 0 deletions src/features/edit-profile/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
57 changes: 57 additions & 0 deletions src/features/edit-profile/ui/DeleteAccountModal.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="max-w-md gap-0 rounded-2xl bg-white p-0"
showCloseButton={false}
>
<DialogHeader className="border-b border-gray-200 p-6">
<DialogTitle className="text-center text-xl font-semibold">
회원 탈퇴
</DialogTitle>
<DialogDescription className="pt-2 text-center text-sm text-gray-500">
탈퇴하면 게시물, 댓글, 좋아요 등 모든 데이터가 삭제되며 복구할 수
없습니다. 정말 탈퇴하시겠어요?
</DialogDescription>
</DialogHeader>

<button
type="button"
className="w-full cursor-pointer border-b border-gray-200 py-4 text-center text-base font-semibold text-red-500"
onClick={() => {
onConfirm()
onOpenChange(false)
}}
>
탈퇴하기
</button>

<button
type="button"
className="w-full cursor-pointer py-4 text-center text-base text-gray-900"
onClick={() => onOpenChange(false)}
>
취소
</button>
</DialogContent>
</Dialog>
)
}
40 changes: 40 additions & 0 deletions src/features/edit-profile/ui/MetaAccountCenter.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<button
type="button"
className="w-full cursor-pointer rounded-2xl bg-white p-6 text-left shadow-[0_2px_16px_rgba(0,0,0,0.06)]"
>
<img src={MetaLogo} alt="Meta" className="mb-4 h-6" />

<h2 className="mb-2 text-lg font-bold text-gray-900">계정 센터</h2>
<p className="mb-4 text-sm text-gray-500">
Meta 테크놀로지 전반에서 연결된 환경 및 계정 설정을 관리해보세요.
</p>

<ul className="mb-4 flex flex-col gap-3">
{menuItems.map((item) => (
<li
key={item.label}
className="flex items-center gap-3 text-sm text-gray-900"
>
<item.icon className="size-5 text-gray-600" />
{item.label}
</li>
))}
</ul>

<span className="text-sm font-medium text-blue-500">
계정 센터에서 더 보기
</span>
</button>
)
}
32 changes: 32 additions & 0 deletions src/features/edit-profile/ui/ProfileEditForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(
currentUser?.profileImageUrl ?? null
)
const [isPhotoModalOpen, setIsPhotoModalOpen] = useState(false)
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)

const originalBio = currentUser?.bio ?? ''
Expand All @@ -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,
Expand Down Expand Up @@ -89,6 +105,12 @@ export function ProfileEditForm() {
onDelete={handleDeletePhoto}
/>

<DeleteAccountModal
open={isDeleteModalOpen}
onOpenChange={setIsDeleteModalOpen}
onConfirm={() => deleteMutation.mutate()}
/>

<ProfileEditCard
avatarUrl={avatarUrl}
nickname={currentUser.nickname}
Expand Down Expand Up @@ -128,6 +150,16 @@ export function ProfileEditForm() {
제출
</button>
</div>

<div className="mt-8 border-t border-gray-200 pt-8">
<button
type="button"
className="cursor-pointer text-sm font-medium text-red-500 hover:text-red-600 hover:underline"
onClick={() => setIsDeleteModalOpen(true)}
>
회원 탈퇴
</button>
</div>
</div>
)
}
42 changes: 42 additions & 0 deletions src/mocks/handlers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down