import bcrypt from 'bcrypt' import { createError, readBody } from 'h3' import { z } from 'zod' import { countOwnerMembers, deleteMember, getUserByIdWithPassword, MEMBER_ROLE } from '../../repositories/member-repository' import { clearAdminSession } from '../../utils/admin-auth' import { clearMemberSession, requireMemberSession } from '../../utils/member-auth' import { removeManagedAvatarAsset } from '../../utils/member-avatar' const deleteAccountSchema = z.object({ password: z.string().min(1) }) /** * 회원 탈퇴 API * @param {import('h3').H3Event} event - 요청 이벤트 * @returns {Promise<{ ok: true }>} 처리 결과 */ export default defineEventHandler(async (event) => { const session = requireMemberSession(event) const parsedBody = deleteAccountSchema.safeParse(await readBody(event)) if (!parsedBody.success) { throw createError({ statusCode: 400, message: '탈퇴 요청 형식이 올바르지 않습니다.' }) } const user = await getUserByIdWithPassword(session.userId) if (!user) { throw createError({ statusCode: 404, message: '회원 정보를 찾을 수 없습니다.' }) } const isPasswordValid = await bcrypt.compare(parsedBody.data.password, user.passwordHash) if (!isPasswordValid) { throw createError({ statusCode: 401, message: '비밀번호가 올바르지 않습니다.' }) } if (user.role === MEMBER_ROLE.OWNER && (await countOwnerMembers()) <= 1) { throw createError({ statusCode: 400, message: '최소 1명의 소유자 권한은 유지되어야 합니다.' }) } if (user.avatarUrl) { await removeManagedAvatarAsset(user.avatarUrl) } await deleteMember(session.userId) clearMemberSession(event) clearAdminSession(event) return { ok: true } })