diff --git a/src/entities/projects/api/getProjectLikeApi.ts b/src/entities/projects/api/getProjectLikeApi.ts index 48a0e7e..23414ca 100644 --- a/src/entities/projects/api/getProjectLikeApi.ts +++ b/src/entities/projects/api/getProjectLikeApi.ts @@ -143,14 +143,11 @@ export const deleteUserLikes = async ( ): Promise => { if (!userId || !projectIds.length) return; - const q = query( - collection(db, "likes"), - where("userId", "==", userId), - where("projectId", "in", projectIds) - ); - const snapshot = await getDocs(q); - const deletePromises = snapshot.docs.map((docSnap) => - deleteDoc(doc(db, "likes", docSnap.id)) - ); + // 문서 ID를 직접 생성하여 삭제 (더 효율적) + const deletePromises = projectIds.map((projectId) => { + const likeId = `${userId}_${projectId}`; + return deleteDoc(doc(db, "likes", likeId)); + }); + await Promise.all(deletePromises); }; diff --git a/src/entities/projects/api/projectsApi.ts b/src/entities/projects/api/projectsApi.ts index dba74cd..4446b70 100644 --- a/src/entities/projects/api/projectsApi.ts +++ b/src/entities/projects/api/projectsApi.ts @@ -11,6 +11,9 @@ import { doc, getDoc, where, + deleteDoc, + updateDoc, + arrayRemove, } from "firebase/firestore"; import { db } from "@shared/firebase/firebase"; @@ -111,3 +114,63 @@ export const getProjectsByIds = async ( ); return results; }; + +/** 여러 프로젝트를 완전히 삭제 (likes, applications, projects, users 컬렉션 모두) */ +export const deleteProjectsEverywhere = async ( + projectIds: string[], + userId: string +): Promise<{ success: boolean; error?: string }> => { + try { + // 모든 삭제 작업을 병렬로 실행하기 위한 함수들 + + // 좋아요 삭제 - 프로젝트 ID로 모든 좋아요 찾아서 삭제 + const deleteLikesForProject = async ( + projectId: string + ): Promise => { + const likesSnap = await getDocs( + query(collection(db, "likes"), where("projectId", "==", projectId)) + ); + return Promise.all(likesSnap.docs.map((doc) => deleteDoc(doc.ref))); + }; + + const deleteApplicationsForProject = async ( + projectId: string + ): Promise => { + const appsSnap = await getDocs( + query( + collection(db, "applications"), + where("projectId", "==", projectId) + ) + ); + return Promise.all(appsSnap.docs.map((doc) => deleteDoc(doc.ref))); + }; + + const deleteProject = async (projectId: string): Promise => { + return deleteDoc(doc(db, "projects", projectId)); + }; + + // 모든 작업을 병렬로 실행 + await Promise.all([ + // 1. likes 컬렉션에서 모든 프로젝트의 likes 삭제 (병렬) + ...projectIds.map(deleteLikesForProject), + + // 2. applications 컬렉션에서 모든 프로젝트의 applications 삭제 (병렬) + ...projectIds.map(deleteApplicationsForProject), + + // 3. projects 컬렉션에서 모든 프로젝트 삭제 (병렬) + ...projectIds.map(deleteProject), + + // 4. users 컬렉션에서 myProjects, likeProjects, appliedProjects에서 제거 + updateDoc(doc(db, "users", userId), { + myProjects: arrayRemove(...projectIds), + likeProjects: arrayRemove(...projectIds), + appliedProjects: arrayRemove(...projectIds), + }), + ]); + + return { success: true }; + } catch (err) { + console.error(err); + return { success: false, error: "프로젝트 완전 삭제 실패" }; + } +}; diff --git a/src/entities/projects/hooks/useDeleteProjectsMutation.ts b/src/entities/projects/hooks/useDeleteProjectsMutation.ts new file mode 100644 index 0000000..8aa358a --- /dev/null +++ b/src/entities/projects/hooks/useDeleteProjectsMutation.ts @@ -0,0 +1,204 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import type { UseMutationResult } from "@tanstack/react-query"; + +import { deleteApplication } from "@entities/projects/api/getProjectApplicationsApi"; +import { deleteUserLikes } from "@entities/projects/api/getProjectLikeApi"; +import { deleteProjectsEverywhere } from "@entities/projects/api/projectsApi"; + +import queryKeys from "@shared/react-query/queryKey"; +import { useLikeStore } from "@shared/stores/likeStore"; +import { useProjectStore } from "@shared/stores/projectStore"; +import { useSnackbarStore } from "@shared/stores/snackbarStore"; +import { ProjectCollectionTabType } from "@shared/types/project"; +import type { ProjectListRes } from "@shared/types/project"; + +interface DeleteProjectsParams { + type: ProjectCollectionTabType; + ids: string[]; + user: { uid: string } | null; + appliedProjectsData?: ProjectListRes[]; + myLikedProjectsData?: ProjectListRes[]; +} + +const ERROR_MSG = "프로젝트 삭제에 실패했습니다."; + +export const useDeleteProjectsMutation = (): UseMutationResult< + void, + unknown, + DeleteProjectsParams +> => { + const queryClient = useQueryClient(); + const { removeLikeProjects } = useLikeStore(); + const { setAppliedProjects, setLikeProjects } = useProjectStore(); + const { showSuccess, showError } = useSnackbarStore(); + + // 관심 프로젝트 삭제 + const deleteLikes = async ( + userUid: string, + ids: string[], + myLikedProjectsData?: ProjectListRes[] + ): Promise => { + await deleteUserLikes(userUid, ids); + + // 전역 상태 동기화 + removeLikeProjects(ids); + setLikeProjects( + myLikedProjectsData?.filter((p: ProjectListRes) => !ids.includes(p.id)) || + [] + ); + + showSuccess("관심 프로젝트가 삭제되었습니다."); + }; + + // 지원한 프로젝트 삭제 + const deleteApplied = async ( + userUid: string, + ids: string[], + appliedProjectsData?: ProjectListRes[] + ): Promise => { + // applications 컬렉션에서 제거 (병렬 처리) + const deletePromises = ids.map((projectId) => + deleteApplication(userUid, projectId) + ); + await Promise.all(deletePromises); + + // 전역 상태 동기화 + setAppliedProjects( + appliedProjectsData?.filter((p: ProjectListRes) => !ids.includes(p.id)) || + [] + ); + + showSuccess("지원한 프로젝트가 삭제되었습니다."); + }; + + // 만든 프로젝트 삭제 + const deleteCreated = async ( + userUid: string, + ids: string[], + appliedProjectsData?: ProjectListRes[], + myLikedProjectsData?: ProjectListRes[] + ): Promise => { + const res = await deleteProjectsEverywhere(ids, userUid); + + if (!res.success) { + showError(res.error || ERROR_MSG); + throw new Error(res.error || ERROR_MSG); + } + + // 전역 상태 동기화 + setAppliedProjects( + appliedProjectsData?.filter((p: ProjectListRes) => !ids.includes(p.id)) || + [] + ); + setLikeProjects( + myLikedProjectsData?.filter((p: ProjectListRes) => !ids.includes(p.id)) || + [] + ); + removeLikeProjects(ids); + + showSuccess("만든 프로젝트가 삭제되었습니다."); + }; + + // 쿼리 무효화 함수들 + const invalidateLikeQueries = async (): Promise => { + const queries = [ + [queryKeys.myLikedProjects, "details"], + [queryKeys.myLikedProjects, "ids"], + [queryKeys.projectLike], + [queryKeys.projectLikedUser], + [queryKeys.projects], // 홈페이지, 프로젝트 찾기 페이지 동기화 + ]; + + await Promise.all( + queries.map((queryKey) => queryClient.invalidateQueries({ queryKey })) + ); + }; + + const invalidateAppliedQueries = async (): Promise => { + const queries = [ + [queryKeys.myAppliedProjects, "details"], + [queryKeys.myAppliedProjects, "ids"], + [queryKeys.projectAppliedUser], + ]; + + await Promise.all( + queries.map((queryKey) => queryClient.invalidateQueries({ queryKey })) + ); + }; + + const invalidateCreatedQueries = async (userUid: string): Promise => { + const queries = [ + [queryKeys.myLikedProjects, "details"], + [queryKeys.myAppliedProjects, "details"], + [queryKeys.projects], + ["userProfile", userUid], + [queryKeys.projectLike], + [queryKeys.projectLikedUser], + [queryKeys.projectAppliedUser], + ]; + + await Promise.all( + queries.map((queryKey) => queryClient.invalidateQueries({ queryKey })) + ); + }; + + // 타입별 쿼리 무효화 + const invalidateQueries = async ( + type: ProjectCollectionTabType, + user?: { uid: string } | null + ): Promise => { + switch (type) { + case ProjectCollectionTabType.Likes: + await invalidateLikeQueries(); + break; + case ProjectCollectionTabType.Applied: + await invalidateAppliedQueries(); + break; + case ProjectCollectionTabType.Created: + if (user) { + await invalidateCreatedQueries(user.uid); + } + break; + } + }; + + // 메인 삭제 로직 + const handleDelete = async ({ + type, + ids, + user, + appliedProjectsData, + myLikedProjectsData, + }: DeleteProjectsParams): Promise => { + if (!user) { + throw new Error("로그인이 필요합니다."); + } + + switch (type) { + case ProjectCollectionTabType.Likes: + await deleteLikes(user.uid, ids, myLikedProjectsData); + break; + case ProjectCollectionTabType.Applied: + await deleteApplied(user.uid, ids, appliedProjectsData); + break; + case ProjectCollectionTabType.Created: + await deleteCreated( + user.uid, + ids, + appliedProjectsData, + myLikedProjectsData + ); + break; + } + }; + + return useMutation({ + mutationFn: handleDelete, + onSuccess: async (_data, variables): Promise => { + await invalidateQueries(variables.type, variables.user); + }, + onError: (error: any): void => { + showError(error?.message || ERROR_MSG); + }, + }); +}; diff --git a/src/entities/projects/queries/useGetProjectLike.ts b/src/entities/projects/queries/useGetProjectLike.ts index 01d9e93..0127111 100644 --- a/src/entities/projects/queries/useGetProjectLike.ts +++ b/src/entities/projects/queries/useGetProjectLike.ts @@ -7,6 +7,7 @@ import { getProjectLikedUsers, getProjectLikeStatus, } from "@entities/projects/api/getProjectLikeApi"; +import { getProjectsByIds } from "@entities/projects/api/projectsApi"; import queryKeys from "@shared/react-query/queryKey"; import { useAuthStore } from "@shared/stores/authStore"; @@ -62,3 +63,13 @@ export const useGetMyLikedProjectsWithDetails = (): UseQueryResult< enabled: !!user, }); }; + +export const useGetMyCreatedProjectsWithDetails = ( + myProjectsIds?: string[] +): UseQueryResult => { + return useQuery({ + queryKey: [queryKeys.myCreatedProjects, "details", myProjectsIds], + queryFn: () => getProjectsByIds(myProjectsIds || []), + enabled: !!myProjectsIds && myProjectsIds.length > 0, + }); +}; diff --git a/src/features/projects/api/projectsApi.ts b/src/features/projects/api/projectsApi.ts index 3abb876..ca9b2c3 100644 --- a/src/features/projects/api/projectsApi.ts +++ b/src/features/projects/api/projectsApi.ts @@ -1,15 +1,10 @@ import { addDoc, - arrayRemove, arrayUnion, collection, doc, serverTimestamp, updateDoc, - getDocs, - where, - deleteDoc, - query, } from "firebase/firestore"; import type { ApiResMessage } from "@entities/projects/types/firebase"; @@ -104,61 +99,3 @@ export const updateApplyOrLike = async ( }; } }; - -/** 여러 프로젝트를 완전히 삭제 (likes, applications, projects, users 컬렉션 모두) */ -export const deleteProjectsEverywhere = async ( - projectIds: string[], - userId: string -): Promise<{ success: boolean; error?: string }> => { - try { - // 모든 삭제 작업을 병렬로 실행하기 위한 함수들 - const deleteLikesForProject = async ( - projectId: string - ): Promise => { - const likesSnap = await getDocs( - query(collection(db, "likes"), where("projectId", "==", projectId)) - ); - return Promise.all(likesSnap.docs.map((doc) => deleteDoc(doc.ref))); - }; - - const deleteApplicationsForProject = async ( - projectId: string - ): Promise => { - const appsSnap = await getDocs( - query( - collection(db, "applications"), - where("projectId", "==", projectId) - ) - ); - return Promise.all(appsSnap.docs.map((doc) => deleteDoc(doc.ref))); - }; - - const deleteProject = async (projectId: string): Promise => { - return deleteDoc(doc(db, "projects", projectId)); - }; - - // 모든 작업을 병렬로 실행 - await Promise.all([ - // 1. likes 컬렉션에서 모든 프로젝트의 likes 삭제 (병렬) - ...projectIds.map(deleteLikesForProject), - - // 2. applications 컬렉션에서 모든 프로젝트의 applications 삭제 (병렬) - ...projectIds.map(deleteApplicationsForProject), - - // 3. projects 컬렉션에서 모든 프로젝트 삭제 (병렬) - ...projectIds.map(deleteProject), - - // 4. users 컬렉션에서 myProjects, likeProjects, appliedProjects에서 제거 - updateDoc(doc(db, "users", userId), { - myProjects: arrayRemove(...projectIds), - likeProjects: arrayRemove(...projectIds), - appliedProjects: arrayRemove(...projectIds), - }), - ]); - - return { success: true }; - } catch (err) { - console.error(err); - return { success: false, error: "프로젝트 완전 삭제 실패" }; - } -}; diff --git a/src/features/projects/ui/ProjectDelete.tsx b/src/features/projects/ui/ProjectDelete.tsx index f8d57c2..069ff00 100644 --- a/src/features/projects/ui/ProjectDelete.tsx +++ b/src/features/projects/ui/ProjectDelete.tsx @@ -26,24 +26,10 @@ export const ProjectDones = ({ } }; - const handleModify = (): void => { - // Navigate '/project/insert로 이동' - // state로 폼 넘김 - // 이푸 state 존재 여부에 따라 등록, 수정 나눌 예정 - // form을 나눈다면 여기서 나눠서 보낼 수 있도록 ... - alert("아직없어염.."); - }; - return ( - - - 수정하기 - - - - 모집 마감 하기 - - + + 모집 마감 하기 + ); }; diff --git a/src/pages/user-profile/ui/UserProfilePage.tsx b/src/pages/user-profile/ui/UserProfilePage.tsx index 6ebf0ab..cda1329 100644 --- a/src/pages/user-profile/ui/UserProfilePage.tsx +++ b/src/pages/user-profile/ui/UserProfilePage.tsx @@ -1,24 +1,20 @@ import { Box, Container, Chip as MuiChip } from "@mui/material"; import { styled as muiStyled } from "@mui/material/styles"; -import { useQueryClient } from "@tanstack/react-query"; import type { JSX } from "react"; -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect } from "react"; -import { deleteProjectsEverywhere } from "@features/projects/api/projectsApi"; - -import { deleteApplication } from "@entities/projects/api/getProjectApplicationsApi"; -import { deleteUserLikes } from "@entities/projects/api/getProjectLikeApi"; -import { useProjectsByIds } from "@entities/projects/hooks/useProjectsByIds"; +import { useDeleteProjectsMutation } from "@entities/projects/hooks/useDeleteProjectsMutation"; import { useGetMyAppliedProjectsWithDetails } from "@entities/projects/queries/useGetProjectApplications"; -import { useGetMyLikedProjectsWithDetails } from "@entities/projects/queries/useGetProjectLike"; +import { + useGetMyLikedProjectsWithDetails, + useGetMyCreatedProjectsWithDetails, +} from "@entities/projects/queries/useGetProjectLike"; import ProjectCollectionContainer from "@entities/projects/ui/project-collection-tab/ProjectCollectionContainer"; import UserProfileCard from "@entities/user/ui/user-profile/UserProfileCard"; import UserProfileHeader from "@entities/user/ui/user-profile/UserProfileHeader"; import { useUserProfile } from "@shared/queries/useUserProfile"; -import queryKeys from "@shared/react-query/queryKey"; import { useAuthStore } from "@shared/stores/authStore"; -import { useLikeStore } from "@shared/stores/likeStore"; import { useProjectStore } from "@shared/stores/projectStore"; import { ProjectCollectionTabType } from "@shared/types/project"; import LoadingSpinner from "@shared/ui/loading-spinner/LoadingSpinner"; @@ -51,101 +47,52 @@ const UserProfilePage = (): JSX.Element => { uid ?? "" ); - // zustand store 사용 - const { setLikeProjects, setAppliedProjects } = useProjectStore(); - const { setLikedProjectIds, removeLikeProjects } = useLikeStore(); - const queryClient = useQueryClient(); - - // 만든 프로젝트 id 배열 - const createdIds = userProfile?.myProjects ?? []; - // 지원한 프로젝트 데이터 가져오기 (applications 컬렉션 기반) const { data: appliedProjectsData, isLoading: appliedProjectsLoading } = useGetMyAppliedProjectsWithDetails(); - // 만든 프로젝트는 기존대로 id 배열로 fetch - const { data: createdProjectsData, isLoading: createdProjectsLoading } = - useProjectsByIds(createdIds); const { data: myLikedProjectsData, isLoading: myLikedProjectsLoading } = useGetMyLikedProjectsWithDetails(); - // zustand store에 동기화 - useEffect(() => { - if (myLikedProjectsData) { - setLikeProjects(myLikedProjectsData); - // 좋아요 프로젝트 ID들도 likeStore에 동기화 - const likedIds = myLikedProjectsData.map((project) => project.id); - setLikedProjectIds(likedIds); - } - }, [myLikedProjectsData, setLikeProjects, setLikedProjectIds]); - useEffect(() => { - if (appliedProjectsData) setAppliedProjects(appliedProjectsData); - }, [appliedProjectsData, setAppliedProjects]); + + // 만든 프로젝트 데이터 가져오기 (userProfile.myProjects 기반) + const { data: myCreatedProjectsData, isLoading: myCreatedProjectsLoading } = + useGetMyCreatedProjectsWithDetails(userProfile?.myProjects); const [tab, setTab] = useState( ProjectCollectionTabType.Likes ); - // 프로젝트 삭제 핸들러 - const handleDeleteProjects = useCallback( - async (type: ProjectCollectionTabType, ids: string[]) => { - if (type === ProjectCollectionTabType.Likes && user) { - await deleteUserLikes(user.uid, ids); - removeLikeProjects(ids); - await queryClient.invalidateQueries({ - queryKey: [queryKeys.myLikedProjects, "details"], - }); - } - if (type === ProjectCollectionTabType.Applied && user) { - for (const projectId of ids) { - await deleteApplication(user.uid, projectId); - } - await queryClient.invalidateQueries({ - queryKey: [queryKeys.myAppliedProjects, "details"], - }); - } - if (type === ProjectCollectionTabType.Created && user) { - // 만든 프로젝트 완전 삭제 - const res = await deleteProjectsEverywhere(ids, user.uid); - if (res.success) { - // zustand store 동기화 - setAppliedProjects( - appliedProjectsData - ? appliedProjectsData.filter((p) => !ids.includes(p.id)) - : [] - ); - setLikeProjects( - myLikedProjectsData - ? myLikedProjectsData.filter((p) => !ids.includes(p.id)) - : [] - ); - // 쿼리 invalidate - await queryClient.invalidateQueries({ - queryKey: [queryKeys.myLikedProjects, "details"], - }); - await queryClient.invalidateQueries({ - queryKey: [queryKeys.myAppliedProjects, "details"], - }); - await queryClient.invalidateQueries({ - queryKey: [queryKeys.projects], - }); - await queryClient.invalidateQueries({ - queryKey: ["userProfile", user.uid], - }); - } else { - alert(res.error || "프로젝트 삭제에 실패했습니다."); - } - } - }, - [ + const deleteProjectsMutation = useDeleteProjectsMutation(); + + // projectStore 동기화 + const { setAppliedProjects, setLikeProjects } = useProjectStore(); + + // 지원한 프로젝트 데이터를 store에 동기화 + useEffect(() => { + if (appliedProjectsData) { + setAppliedProjects(appliedProjectsData); + } + }, [appliedProjectsData, setAppliedProjects]); + + // 좋아요한 프로젝트 데이터를 store에 동기화 + useEffect(() => { + if (myLikedProjectsData) { + setLikeProjects(myLikedProjectsData); + } + }, [myLikedProjectsData, setLikeProjects]); + + const handleDeleteProjects = async ( + type: ProjectCollectionTabType, + ids: string[] + ): Promise => { + await deleteProjectsMutation.mutateAsync({ + type, + ids, user, - removeLikeProjects, - queryClient, - setAppliedProjects, - setLikeProjects, appliedProjectsData, myLikedProjectsData, - ] - ); + }); + }; // 사용자 프로필이 로딩 중이거나 없으면 early return if (userProfileLoading) { @@ -178,11 +125,11 @@ const UserProfilePage = (): JSX.Element => {