Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions src/app/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ body {
"Droid Sans",
"Helvetica Neue",
sans-serif;
background-color: #f8fafc;
}

*,
Expand Down
55 changes: 46 additions & 9 deletions src/features/projects/hook/useApplyFrom.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { useState, type ChangeEvent } from "react";
import { useParams } from "react-router-dom";

import useProjectApply from "@features/projects/queries/useProjectApply";
import { useCancelProjectApplication } from "@features/projects/queries/useCancelProjectApplication";
import { useCreateProjectApplications } from "@features/projects/queries/useCreateProjcetApplications";

import { useGetProjectApplicationStatus } from "@entities/projects/queries/useGetProjectApplications";

import { useSnackbarStore } from "@shared/stores/snackbarStore";

interface ApplyFormResult {
openForm: {
Expand All @@ -13,27 +19,54 @@ interface ApplyFormResult {
update: (e: ChangeEvent<HTMLTextAreaElement>) => void;
};
submit: () => void;
cancle: () => void;
isPending: boolean;
isCancling: boolean;
isApplied: boolean;
}

const useApplyFrom = (projectID: string): ApplyFormResult => {
const { mutate: updateApply } = useProjectApply();
const useApplyFrom = (): ApplyFormResult => {
const { id: projectId } = useParams();
const { showError } = useSnackbarStore();

const { data: isApplied = false, isLoading: dataLoading } =
useGetProjectApplicationStatus();
const { mutate: createProjectApplication, isPending: createPending } =
useCreateProjectApplications();
const { mutate: cancelProjectApplication, isPending: cancelPending } =
useCancelProjectApplication();

const [isFormOpen, setIsFormOpen] = useState(false);
const [applyMessage, setApplyMessage] = useState("");

const openForm = (): void => setIsFormOpen(true);
const openForm = (): void => {
if (dataLoading) return;
setIsFormOpen(true);
};
const closeForm = (): void => setIsFormOpen(false);

const updateApplyMessage = (e: ChangeEvent<HTMLTextAreaElement>): void =>
setApplyMessage(e.target.value);

const submit = (): void => {
if (!projectID) return;
const handleApplySubmit = (): void => {
if (!projectId) return;
if (!applyMessage.trim()) {
alert("메세지를 적어주세요");
showError("메시지를 입력해주세요.");
return;
}
updateApply(projectID);

createProjectApplication(applyMessage.trim(), {
onSuccess: () => closeForm,
});
};

const handleCancelSubmit = (): void => {
if (!projectId) return;

const isRealCancle = confirm("정말로 지원을 취소하시겠습니까?");
if (!isRealCancle) return;

cancelProjectApplication(projectId);
};

return {
Expand All @@ -46,7 +79,11 @@ const useApplyFrom = (projectID: string): ApplyFormResult => {
value: applyMessage,
update: updateApplyMessage,
},
submit,
submit: handleApplySubmit,
cancle: handleCancelSubmit,
isPending: createPending,
isCancling: cancelPending,
isApplied,
};
};

Expand Down
87 changes: 87 additions & 0 deletions src/features/projects/hook/useOptimisticProjectLike.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { useEffect, useRef, useState } from "react";
import { useParams } from "react-router-dom";

import { useToggleProjectLikeSync } from "@features/projects/queries/useCreateProjectLike";

import { useGetProjectLike } from "@entities/projects/queries/useGetProjectLike";

import { useLikeStore } from "@shared/stores/likeStore";
import { useSnackbarStore } from "@shared/stores/snackbarStore";

interface UseOptimisticProjectLikeProps {
isLiked: boolean;
toggleLike: () => void;
}

const DEBOUNCE_DELAY_MS = 100;

export const useOptimisticProjectLike = (): UseOptimisticProjectLikeProps => {
const { id: projectId } = useParams();
const { data: serverLikeStatus, isLoading } = useGetProjectLike();
const { mutate: syncToServer } = useToggleProjectLikeSync();
const { addLikedProject, removeLikedProject } = useLikeStore();
const { showSuccess } = useSnackbarStore();

const [optimisticLikeStatus, setOptimisticLikeStatus] = useState<
boolean | undefined
>(serverLikeStatus);
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
const pendingServerSync = useRef<boolean>(false);

useEffect(() => {
if (serverLikeStatus !== undefined && !pendingServerSync.current) {
setOptimisticLikeStatus(serverLikeStatus);
}
}, [serverLikeStatus]);

useEffect(() => {
// projectId을 바꾸려면 사실상 Url을 직접 바쒀서 진입할 수 밖에 없긴한데
// 이 경우 해당 훅을 불러오는 컴포넌트가 언마운트 되었다가 다시 마운트 되는 구조라
// if문이 실행되는 경우를 알 수가 없지만
// pendingServerSync.current = false;를 꼭 해야한다면
// 의존성 [] 이어도 될 같습니다 ... 만 나중에 천천히 알아보며 리팩토링 하기로하고 남겨두겟습니다
return () => {
if (debounceTimerRef.current) {
console.log("???//sDFsdfsdf");
clearTimeout(debounceTimerRef.current);
}
pendingServerSync.current = false;
};
}, [projectId]);

const toggleLike = (): void => {
if (!projectId || isLoading) return;

const newLikeStatus = !optimisticLikeStatus;
setOptimisticLikeStatus(newLikeStatus);

// 전역 상태도 업데이트
if (newLikeStatus) {
addLikedProject(projectId);
showSuccess("좋아요 되었습니다.");
} else {
removeLikedProject(projectId);
showSuccess("좋아요가 취소 되었습니다.");
}

if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}

pendingServerSync.current = true;
debounceTimerRef.current = setTimeout(() => {
syncToServer(projectId, {
onSettled: () => {
pendingServerSync.current = false;
},
});
}, DEBOUNCE_DELAY_MS);
};

const displayLikeStatus = optimisticLikeStatus ?? serverLikeStatus ?? false;

return {
isLiked: displayLikeStatus,
toggleLike,
};
};
5 changes: 5 additions & 0 deletions src/features/projects/queries/useCancelProjectApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import queryKeys from "@shared/react-query/queryKey";
import { useApplicationsStore } from "@shared/stores/applicationsStore";
import { useAuthStore } from "@shared/stores/authStore";
import { useSnackbarStore } from "@shared/stores/snackbarStore";

import { cancelProjectApplications } from "../api/createProjectApplicationsApi";

Expand All @@ -21,6 +22,7 @@ export const useCancelProjectApplication = (): UseMutationResult<
> => {
const { user } = useAuthStore();
const { removeAppliedProject } = useApplicationsStore();
const { showError, showSuccess } = useSnackbarStore();
const queryClient = useQueryClient();

return useMutation({
Expand All @@ -29,6 +31,8 @@ export const useCancelProjectApplication = (): UseMutationResult<
},
onSuccess: (_, projectId) => {
removeAppliedProject(projectId);
showSuccess("지원이 취소되었습니다.");

queryClient.invalidateQueries({
queryKey: [queryKeys.myAppliedProjects],
});
Expand All @@ -41,6 +45,7 @@ export const useCancelProjectApplication = (): UseMutationResult<
},
onError: (error) => {
console.error("지원 취소 실패:", error);
showError(`지원 취소 실패: ${error.message}`);
},
});
};
43 changes: 31 additions & 12 deletions src/features/projects/queries/useCreateProjcetApplications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,60 @@ import {
useQueryClient,
type UseMutationResult,
} from "@tanstack/react-query";
import type { User } from "firebase/auth";
import { useParams } from "react-router-dom";

import { createProjectApplication } from "@features/projects/api/createProjectApplicationsApi";

import queryKeys from "@shared/react-query/queryKey";
import { useApplicationsStore } from "@shared/stores/applicationsStore";
import type { CreateProjectApplicationInput } from "@shared/types/project";
import { useAuthStore } from "@shared/stores/authStore";
import { useSnackbarStore } from "@shared/stores/snackbarStore";

/**
* 프로젝트 지원 생성 훅
* @returns UseMutationResult<void, Error, CreateProjectApplicationInput> - 프로젝트 지원 생성 결과
* @returns UseMutationResult<void, Error, string> - 프로젝트 지원 생성 결과
*/
export const useCreateProjectApplications = (): UseMutationResult<
void,
Error,
CreateProjectApplicationInput
string
> => {
const queryClient = useQueryClient();
const { id: projectId } = useParams();
const { user } = useAuthStore();
const { addAppliedProject } = useApplicationsStore();
const { showError, showSuccess } = useSnackbarStore();
const queryClient = useQueryClient();

return useMutation({
mutationFn: (input: CreateProjectApplicationInput) => {
return createProjectApplication(input);
},
mutationFn: (message: string) => {
if (!projectId) {
return Promise.reject(new Error("projectId가 없서요."));
}

onSuccess: (_data, input) => {
addAppliedProject(input.projectId);
return createProjectApplication({
userId: user?.uid as User["uid"],
projectId,
message,
});
},

onSettled: (_data, _error, input) => {
onSuccess: (_data) => {
if (!projectId) return;
addAppliedProject(projectId);
showSuccess("지원이 완료되었습니다! 🎉");
},
onSettled: (_data, _error) => {
queryClient.invalidateQueries({
queryKey: [queryKeys.projectApply, input.projectId],
queryKey: [queryKeys.projectApply, projectId],
});
queryClient.invalidateQueries({
queryKey: [queryKeys.projectAppliedUser, input.projectId],
queryKey: [queryKeys.projectAppliedUser, projectId],
});
},
onError: (error) => {
console.error("지원 실패:", error);
showError(`지원 실패: ${error.message}`);
},
});
};
83 changes: 0 additions & 83 deletions src/features/projects/queries/useCreateProjectLike.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,13 @@ import {
useQueryClient,
type UseMutationResult,
} from "@tanstack/react-query";
import { useCallback, useRef, useState, useEffect } from "react";
import { useParams } from "react-router-dom";

import { toggleProjectLike } from "@features/projects/api/createProjectLikeApi";

import { useGetProjectLike } from "@entities/projects/queries/useGetProjectLike";

import queryKeys from "@shared/react-query/queryKey";
import { useAuthStore } from "@shared/stores/authStore";
import { useLikeStore } from "@shared/stores/likeStore";
import type { ToggleProjectLikeResponse } from "@shared/types/like";

const DEBOUNCE_DELAY_MS = 100;

export const useToggleProjectLikeSync = (): UseMutationResult<
ToggleProjectLikeResponse,
Error,
Expand Down Expand Up @@ -44,79 +37,3 @@ export const useToggleProjectLikeSync = (): UseMutationResult<
},
});
};

interface UseOptimisticProjectLikeProps {
isLiked: boolean;
isLoading: boolean;
toggleLike: () => void;
}

export const useOptimisticProjectLike = (): UseOptimisticProjectLikeProps => {
const { id: projectId } = useParams();
const { data: serverLikeStatus, isLoading } = useGetProjectLike();
const { mutate: syncToServer } = useToggleProjectLikeSync();
const { addLikedProject, removeLikedProject } = useLikeStore();

const [optimisticLikeStatus, setOptimisticLikeStatus] = useState<
boolean | undefined
>(serverLikeStatus);
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
const pendingServerSync = useRef<boolean>(false);

useEffect(() => {
if (serverLikeStatus !== undefined && !pendingServerSync.current) {
setOptimisticLikeStatus(serverLikeStatus);
}
}, [serverLikeStatus]);

useEffect(() => {
return () => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
pendingServerSync.current = false;
};
}, [projectId]);

const toggleLike = useCallback(() => {
if (!projectId || isLoading) return;

const newLikeStatus = !optimisticLikeStatus;
setOptimisticLikeStatus(newLikeStatus);

// 전역 상태도 업데이트
if (newLikeStatus) {
addLikedProject(projectId);
} else {
removeLikedProject(projectId);
}

if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}

pendingServerSync.current = true;
debounceTimerRef.current = setTimeout(() => {
syncToServer(projectId, {
onSettled: () => {
pendingServerSync.current = false;
},
});
}, DEBOUNCE_DELAY_MS);
}, [
projectId,
isLoading,
optimisticLikeStatus,
addLikedProject,
removeLikedProject,
syncToServer,
]);

const displayLikeStatus = optimisticLikeStatus ?? serverLikeStatus ?? false;

return {
isLiked: displayLikeStatus,
isLoading,
toggleLike,
};
};
Loading