From c2777d18cf5a419ba126700204c40aee6ea8be91 Mon Sep 17 00:00:00 2001 From: cccwon2 Date: Fri, 6 Dec 2024 09:38:03 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20useAuth,=20useUser=20=EA=B0=81=20?= =?UTF-8?q?=ED=9B=85=EC=9D=84=20api=20=ED=8F=B4=EB=8D=94=EC=97=90=20?= =?UTF-8?q?=EB=A7=9E=EA=B2=8C=20=EC=97=AD=ED=95=A0=20=EB=B6=84=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(auth)/login/page.tsx | 8 +- src/app/(auth)/signup/applicant/page.tsx | 8 +- src/app/(auth)/signup/owner/page.tsx | 8 +- .../mypage/components/FilterBar/index.tsx | 4 +- .../components/sections/CommentsSection.tsx | 3 +- .../components/sections/PostsSection.tsx | 3 +- .../components/sections/ScrapsSection.tsx | 3 +- src/app/components/layout/Header.tsx | 6 +- .../modal/modals/form/ChangePasswordModal.tsx | 52 ++--- .../modal/modals/form/EditMyProfileModal.tsx | 72 ++----- .../modals/form/EditOwnerProfileModal.tsx | 80 ++------ src/hooks/queries/auth/useLogin.ts | 44 +++++ src/hooks/queries/auth/useLogout.ts | 30 +++ src/hooks/queries/auth/useRefreshToken.ts | 27 +++ src/hooks/queries/auth/useSignup.ts | 39 ++++ .../queries/user/me/useMyApplications.ts | 29 +++ src/hooks/queries/user/me/useMyComments.ts | 24 +++ src/hooks/queries/user/me/useMyForms.ts | 36 ++++ src/hooks/queries/user/me/useMyPosts.ts | 29 +++ src/hooks/queries/user/me/useMyScraps.ts | 35 ++++ src/hooks/queries/user/me/usePassword.ts | 39 ++++ src/hooks/queries/user/me/useUser.ts | 124 ++++++++++++ src/hooks/useAuth.ts | 126 ------------ src/hooks/useUser.ts | 181 ------------------ 24 files changed, 530 insertions(+), 480 deletions(-) create mode 100644 src/hooks/queries/auth/useLogin.ts create mode 100644 src/hooks/queries/auth/useLogout.ts create mode 100644 src/hooks/queries/auth/useRefreshToken.ts create mode 100644 src/hooks/queries/auth/useSignup.ts create mode 100644 src/hooks/queries/user/me/useMyApplications.ts create mode 100644 src/hooks/queries/user/me/useMyComments.ts create mode 100644 src/hooks/queries/user/me/useMyForms.ts create mode 100644 src/hooks/queries/user/me/useMyPosts.ts create mode 100644 src/hooks/queries/user/me/useMyScraps.ts create mode 100644 src/hooks/queries/user/me/usePassword.ts create mode 100644 src/hooks/queries/user/me/useUser.ts delete mode 100644 src/hooks/useAuth.ts delete mode 100644 src/hooks/useUser.ts diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index 10552b0a..350a5a15 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -1,5 +1,5 @@ "use client"; -import { useAuth } from "@/hooks/useAuth"; +import { useLogin } from "@/hooks/queries/auth/useLogin"; import { type LoginSchema, loginSchema } from "@/schemas/authSchema"; import { zodResolver } from "@hookform/resolvers/zod"; import Image from "next/image"; @@ -7,7 +7,7 @@ import Link from "next/link"; import { useForm } from "react-hook-form"; export default function LoginPage() { - const { login, isLoginPending } = useAuth(); + const { login, isPending } = useLogin(); const { register, handleSubmit, @@ -57,10 +57,10 @@ export default function LoginPage() {
diff --git a/src/app/(auth)/signup/applicant/page.tsx b/src/app/(auth)/signup/applicant/page.tsx index 85e32bf4..2d2a79b3 100644 --- a/src/app/(auth)/signup/applicant/page.tsx +++ b/src/app/(auth)/signup/applicant/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useAuth } from "@/hooks/useAuth"; +import { useSignup } from "@/hooks/queries/auth/useSignup"; import { type SignupSchema, signupSchema } from "@/schemas/authSchema"; import { userRoles } from "@/constants/userRoles"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -9,7 +9,7 @@ import { useForm } from "react-hook-form"; import Image from "next/image"; export default function ApplicantSignupPage() { - const { signup, isSignupPending } = useAuth(); + const { signup, isPending } = useSignup(); const { register, handleSubmit, @@ -100,10 +100,10 @@ export default function ApplicantSignupPage() {
diff --git a/src/app/(auth)/signup/owner/page.tsx b/src/app/(auth)/signup/owner/page.tsx index 0c94e3f1..967d4970 100644 --- a/src/app/(auth)/signup/owner/page.tsx +++ b/src/app/(auth)/signup/owner/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useAuth } from "@/hooks/useAuth"; +import { useSignup } from "@/hooks/queries/auth/useSignup"; import { type SignupSchema, signupSchema } from "@/schemas/authSchema"; import { userRoles } from "@/constants/userRoles"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -9,7 +9,7 @@ import { useForm } from "react-hook-form"; import Image from "next/image"; export default function OwnerSignupPage() { - const { signup, isSignupPending } = useAuth(); + const { signup, isPending } = useSignup(); const { register, handleSubmit, @@ -122,10 +122,10 @@ export default function OwnerSignupPage() {
diff --git a/src/app/(pages)/mypage/components/FilterBar/index.tsx b/src/app/(pages)/mypage/components/FilterBar/index.tsx index e035750a..c45c8dc0 100644 --- a/src/app/(pages)/mypage/components/FilterBar/index.tsx +++ b/src/app/(pages)/mypage/components/FilterBar/index.tsx @@ -6,14 +6,14 @@ import Button from "@/app/components/button/default/Button"; import KebabDropdown from "@/app/components/button/dropdown/KebabDropdown"; import { userRoles } from "@/constants/userRoles"; import useModalStore from "@/store/modalStore"; -import { useUser } from "@/hooks/useUser"; +import { useUser } from "@/hooks/queries/user/me/useUser"; export default function FilterBar() { const { user, isLoading } = useUser(); const { openModal } = useModalStore(); if (isLoading) { - return
Loading...
; + return null; } if (!user) { diff --git a/src/app/(pages)/mypage/components/sections/CommentsSection.tsx b/src/app/(pages)/mypage/components/sections/CommentsSection.tsx index 393203e6..91f8b769 100644 --- a/src/app/(pages)/mypage/components/sections/CommentsSection.tsx +++ b/src/app/(pages)/mypage/components/sections/CommentsSection.tsx @@ -2,7 +2,7 @@ import React from "react"; import { useState } from "react"; -import { useUser } from "@/hooks/useUser"; +import { useMyComments } from "@/hooks/queries/user/me/useMyComments"; import Pagination from "@/app/components/pagination/Pagination"; import type { MyCommentType } from "@/types/response/user"; @@ -14,7 +14,6 @@ export default function CommentsSection() { const [currentPage, setCurrentPage] = useState(1); // 내가 작성한 댓글 목록 조회 - const { useMyComments } = useUser(); const { data, isLoading, error } = useMyComments({ page: currentPage, pageSize: COMMENTS_PER_PAGE, diff --git a/src/app/(pages)/mypage/components/sections/PostsSection.tsx b/src/app/(pages)/mypage/components/sections/PostsSection.tsx index acb7def7..edf826f2 100644 --- a/src/app/(pages)/mypage/components/sections/PostsSection.tsx +++ b/src/app/(pages)/mypage/components/sections/PostsSection.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from "react"; import { useInView } from "react-intersection-observer"; -import { useUser } from "@/hooks/useUser"; +import { useMyPosts } from "@/hooks/queries/user/me/useMyPosts"; import { useSortStore } from "@/store/sortStore"; import type { PostListType } from "@/types/response/post"; @@ -21,7 +21,6 @@ export default function PostsSection() { }); // 내가 작성한 게시글 목록 조회 - const { useMyPosts } = useUser(); const { data, isLoading, error, hasNextPage, fetchNextPage, isFetchingNextPage } = useMyPosts({ limit: POSTS_PER_PAGE, orderBy: orderBy.posts, diff --git a/src/app/(pages)/mypage/components/sections/ScrapsSection.tsx b/src/app/(pages)/mypage/components/sections/ScrapsSection.tsx index 242a2c6e..645bf98f 100644 --- a/src/app/(pages)/mypage/components/sections/ScrapsSection.tsx +++ b/src/app/(pages)/mypage/components/sections/ScrapsSection.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from "react"; import { useInView } from "react-intersection-observer"; -import { useUser } from "@/hooks/useUser"; +import { useMyScraps } from "@/hooks/queries/user/me/useMyScraps"; import { useSortStore } from "@/store/sortStore"; import type { FormListType } from "@/types/response/form"; @@ -21,7 +21,6 @@ export default function ScrapsSection() { }); // 내가 스크랩한 알바폼 목록 조회 - const { useMyScraps } = useUser(); const { data, isLoading, error, hasNextPage, fetchNextPage, isFetchingNextPage } = useMyScraps({ limit: SCRAPS_PER_PAGE, orderBy: orderBy.scrap, diff --git a/src/app/components/layout/Header.tsx b/src/app/components/layout/Header.tsx index b74d608f..7f1ad133 100644 --- a/src/app/components/layout/Header.tsx +++ b/src/app/components/layout/Header.tsx @@ -4,13 +4,13 @@ import Image from "next/image"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { cn } from "@/lib/tailwindUtil"; -import { useAuth } from "@/hooks/useAuth"; +import { useLogout } from "@/hooks/queries/auth/useLogout"; import { useState } from "react"; import { toast } from "react-hot-toast"; -import { useUser } from "@/hooks/useUser"; +import { useUser } from "@/hooks/queries/user/me/useUser"; export default function Header() { - const { logout } = useAuth(); + const { logout } = useLogout(); const { user, isLoading } = useUser(); const pathname = usePathname(); const router = useRouter(); diff --git a/src/app/components/modal/modals/form/ChangePasswordModal.tsx b/src/app/components/modal/modals/form/ChangePasswordModal.tsx index 5984943e..f5c5b6f1 100644 --- a/src/app/components/modal/modals/form/ChangePasswordModal.tsx +++ b/src/app/components/modal/modals/form/ChangePasswordModal.tsx @@ -4,10 +4,8 @@ import { passwordSchema } from "@/schemas/commonSchema"; import { z } from "zod"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import axios from "axios"; -import toast from "react-hot-toast"; -import { useState } from "react"; -import { useAuth } from "@/hooks/useAuth"; +import { useLogout } from "@/hooks/queries/auth/useLogout"; +import { usePassword } from "@/hooks/queries/user/me/usePassword"; interface ChangePasswordModalProps { isOpen: boolean; @@ -54,8 +52,8 @@ const defaultFields = [ ] as const; const ChangePasswordModal = ({ isOpen, onClose, className }: ChangePasswordModalProps) => { - const [isSubmitting, setIsSubmitting] = useState(false); - const { logout } = useAuth(); + const { mutate: changePassword, isPending } = usePassword(); + const { logout } = useLogout(); const { register, @@ -75,35 +73,21 @@ const ChangePasswordModal = ({ isOpen, onClose, className }: ChangePasswordModal if (!isOpen) return null; const onSubmitHandler = async (data: ChangePasswordFormData) => { - if (isSubmitting) return; + if (isPending) return; - try { - setIsSubmitting(true); - await axios.patch("/api/users/me/password", { + changePassword( + { currentPassword: data.currentPassword, newPassword: data.newPassword, - }); - - reset(); - onClose(); - // 비밀번호 변경 후 로그아웃 처리 - logout(); - toast.success("비밀번호가 변경되었습니다!\n다시 로그인해주세요.", { - style: { - whiteSpace: "pre-line", // \n을 줄바꿈으로 처리 - textAlign: "center", // 텍스트 중앙 정렬 + }, + { + onSuccess: () => { + reset(); + onClose(); + logout(); }, - }); - } catch (error) { - if (axios.isAxiosError(error)) { - const errormessage = error.response?.data?.message || "비밀번호 변경에 실패했습니다."; - toast.error(errormessage); - } else { - toast.error("비밀번호 변경 중 오류가 발생했습니다."); } - } finally { - setIsSubmitting(false); - } + ); }; return ( @@ -124,7 +108,7 @@ const ChangePasswordModal = ({ isOpen, onClose, className }: ChangePasswordModal type={field.type} placeholder={field.placeholder} variant="white" - disabled={isSubmitting} + disabled={isPending} size="w-[327px] h-[54px] md:w-[640px] md:h-[64px]" errormessage={errors[field.name]?.message} /> @@ -138,17 +122,17 @@ const ChangePasswordModal = ({ isOpen, onClose, className }: ChangePasswordModal onClose(); reset(); }} - disabled={isSubmitting} + disabled={isPending} className="text-grayscale-700 flex-1 rounded-md border border-grayscale-300 bg-white px-4 py-2 text-sm font-semibold transition-colors hover:bg-grayscale-50 md:text-base" > 취소
diff --git a/src/app/components/modal/modals/form/EditMyProfileModal.tsx b/src/app/components/modal/modals/form/EditMyProfileModal.tsx index 44c5acd8..ea2f2bb3 100644 --- a/src/app/components/modal/modals/form/EditMyProfileModal.tsx +++ b/src/app/components/modal/modals/form/EditMyProfileModal.tsx @@ -5,9 +5,7 @@ import { useState, useRef, useEffect } from "react"; import Image from "next/image"; import { FiUser, FiEdit2 } from "react-icons/fi"; import BaseInput from "@/app/components/input/text/BaseInput"; -import { useUser } from "@/hooks/useUser"; -import axios from "axios"; -import toast from "react-hot-toast"; +import { useUser } from "@/hooks/queries/user/me/useUser"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; @@ -28,10 +26,9 @@ const editMyProfileSchema = z.object({ type EditMyProfileFormData = z.infer; const EditMyProfileModal = ({ isOpen, onClose, className }: EditMyProfileModalProps) => { - const { user, refetch } = useUser(); + const { user, updateProfile, isUpdating } = useUser(); const [selectedFile, setSelectedFile] = useState(null); const [previewUrl, setPreviewUrl] = useState(""); - const [isSubmitting, setIsSubmitting] = useState(false); const fileInputRef = useRef(null); const { @@ -78,58 +75,19 @@ const EditMyProfileModal = ({ isOpen, onClose, className }: EditMyProfileModalPr }; const onSubmitHandler = async (data: EditMyProfileFormData) => { - if (isSubmitting) return; + if (isUpdating) return; - try { - setIsSubmitting(true); - - let imageUrl = user?.imageUrl || ""; - - if (selectedFile) { - const uploadFormData = new FormData(); - uploadFormData.append("image", selectedFile); - - const uploadResponse = await axios.post("/api/images/upload", uploadFormData, { - withCredentials: true, - }); - - if (uploadResponse.status === 201 && uploadResponse.data?.url) { - imageUrl = uploadResponse.data.url; - } else { - throw new Error("이미지 업로드에 실패했습니다."); - } - } - - const updateData = { + const success = await updateProfile( + { name: data.name, nickname: data.nickname, phoneNumber: data.phone, - imageUrl, - }; - - const updateResponse = await axios.patch("/api/users/me", updateData); - - if (updateResponse.status === 200) { - await refetch(); // React Query 캐시 갱신 - toast.success("프로필이 성공적으로 수정되었습니다."); - onClose(); - } else { - throw new Error("프로필 업데이트에 실패했습니다."); - } - } catch (error) { - if (axios.isAxiosError(error)) { - const errorMessage = error.response?.data?.message || "프로필 수정에 실패했습니다."; - console.error("Profile update error:", { - status: error.response?.status, - data: error.response?.data, - }); - toast.error(errorMessage); - } else { - console.error("Unexpected error:", error); - toast.error("프로필 수정 중 오류가 발생했습니다."); - } - } finally { - setIsSubmitting(false); + }, + selectedFile + ); + + if (success) { + onClose(); } }; @@ -201,7 +159,7 @@ const EditMyProfileModal = ({ isOpen, onClose, className }: EditMyProfileModalPr variant="white" size="w-[327px] h-[54px] md:w-[640px] md:h-[64px]" wrapperClassName="px-[14px] md:px-[20px]" - disabled={isSubmitting} + disabled={isUpdating} errormessage={errors[field.name]?.message} />
@@ -213,17 +171,17 @@ const EditMyProfileModal = ({ isOpen, onClose, className }: EditMyProfileModalPr
diff --git a/src/app/components/modal/modals/form/EditOwnerProfileModal.tsx b/src/app/components/modal/modals/form/EditOwnerProfileModal.tsx index 45dc8c3d..74575117 100644 --- a/src/app/components/modal/modals/form/EditOwnerProfileModal.tsx +++ b/src/app/components/modal/modals/form/EditOwnerProfileModal.tsx @@ -5,9 +5,7 @@ import { useState, useRef, useEffect } from "react"; import Image from "next/image"; import { FiUser, FiEdit2, FiMapPin } from "react-icons/fi"; import BaseInput from "@/app/components/input/text/BaseInput"; -import { useUser } from "@/hooks/useUser"; -import axios from "axios"; -import toast from "react-hot-toast"; +import { useUser } from "@/hooks/queries/user/me/useUser"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; @@ -39,10 +37,9 @@ type Field = { }; const EditOwnerProfileModal = ({ isOpen, onClose, className }: EditOwnerProfileModalProps) => { - const { user, refetch } = useUser(); + const { user, updateProfile, isUpdating } = useUser(); const [selectedFile, setSelectedFile] = useState(null); const [previewUrl, setPreviewUrl] = useState(""); - const [isSubmitting, setIsSubmitting] = useState(false); const fileInputRef = useRef(null); const { @@ -90,56 +87,21 @@ const EditOwnerProfileModal = ({ isOpen, onClose, className }: EditOwnerProfileM }; const onSubmitHandler = async (data: EditOwnerProfileFormData) => { - if (isSubmitting) return; - - try { - setIsSubmitting(true); - - let imageUrl = user?.imageUrl || ""; - - if (selectedFile) { - const uploadFormData = new FormData(); - uploadFormData.append("image", selectedFile); - - const uploadResponse = await axios.post("/api/images/upload", uploadFormData, { - withCredentials: true, - }); - - if (uploadResponse.status === 201 && uploadResponse.data?.url) { - imageUrl = uploadResponse.data.url; - } else { - throw new Error("이미지 업로드에 실패했습니다."); - } - } - - const updateData = { - ...data, - imageUrl, - }; - - const updateResponse = await axios.patch("/api/users/me", updateData); - - if (updateResponse.status === 200) { - await refetch(); // React Query 캐시 갱신 - toast.success("사장님 정보가 성공적으로 수정되었습니다."); - onClose(); - } else { - throw new Error("프로필 업데이트에 실패했습니다."); - } - } catch (error) { - if (axios.isAxiosError(error)) { - const errorMessage = error.response?.data?.message || "정보 수정에 실패했습니다."; - console.error("Profile update error:", { - status: error.response?.status, - data: error.response?.data, - }); - toast.error(errorMessage); - } else { - console.error("Unexpected error:", error); - toast.error("정보 수정 중 오류가 발생했습니다."); - } - } finally { - setIsSubmitting(false); + if (isUpdating) return; + + const success = await updateProfile( + { + nickname: data.nickname, + storeName: data.storeName, + storePhoneNumber: data.storePhoneNumber, + phoneNumber: data.phoneNumber, + location: data.location, + }, + selectedFile + ); + + if (success) { + onClose(); } }; @@ -225,7 +187,7 @@ const EditOwnerProfileModal = ({ isOpen, onClose, className }: EditOwnerProfileM variant="white" size="h-[54px] w-[327px] md:h-[64px] md:w-[640px]" wrapperClassName={`px-[14px] md:px-[20px] ${field.icon ? "pl-[40px] md:pl-[48px]" : ""}`} - disabled={isSubmitting} + disabled={isUpdating} errormessage={errors[field.name]?.message} /> {field.icon &&
{field.icon}
} @@ -244,17 +206,17 @@ const EditOwnerProfileModal = ({ isOpen, onClose, className }: EditOwnerProfileM diff --git a/src/hooks/queries/auth/useLogin.ts b/src/hooks/queries/auth/useLogin.ts new file mode 100644 index 00000000..479414fe --- /dev/null +++ b/src/hooks/queries/auth/useLogin.ts @@ -0,0 +1,44 @@ +import { LoginSchema } from "@/schemas/authSchema"; +import { AuthResponse } from "@/types/response/auth"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import axios from "axios"; +import { useRouter } from "next/navigation"; +import { toast } from "react-hot-toast"; + +export const useLogin = () => { + const router = useRouter(); + const queryClient = useQueryClient(); + + const loginMutation = useMutation({ + mutationFn: async (data: LoginSchema) => { + try { + const response = await axios.post("/api/auth/login", data, { + withCredentials: false, + }); + return response.data; + } catch (error) { + if (axios.isAxiosError(error)) { + throw new Error(error.response?.data?.message || "로그인 중 오류가 발생했습니다."); + } + throw error; + } + }, + onSuccess: (data) => { + if (data?.user) { + queryClient.setQueryData(["user"], { user: data.user }); + toast.success("로그인되었습니다!"); + router.push("/"); + router.refresh(); + } + }, + onError: (error: Error) => { + toast.error(error.message); + }, + }); + + return { + login: (data: LoginSchema) => loginMutation.mutate(data), + isPending: loginMutation.isPending, + error: loginMutation.error, + }; +}; diff --git a/src/hooks/queries/auth/useLogout.ts b/src/hooks/queries/auth/useLogout.ts new file mode 100644 index 00000000..46c3bb93 --- /dev/null +++ b/src/hooks/queries/auth/useLogout.ts @@ -0,0 +1,30 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import axios from "axios"; +import { useRouter } from "next/navigation"; +import { toast } from "react-hot-toast"; + +export const useLogout = () => { + const router = useRouter(); + const queryClient = useQueryClient(); + + const logoutMutation = useMutation({ + mutationFn: async () => { + const response = await axios.post("/api/auth/logout"); + return response.data; + }, + onSuccess: () => { + queryClient.clear(); + router.push("/"); + router.refresh(); + }, + onError: (error: Error) => { + toast.error(error.message || "로그아웃 중 오류가 발생했습니다."); + }, + }); + + return { + logout: () => logoutMutation.mutate(), + isPending: logoutMutation.isPending, + error: logoutMutation.error, + }; +}; diff --git a/src/hooks/queries/auth/useRefreshToken.ts b/src/hooks/queries/auth/useRefreshToken.ts new file mode 100644 index 00000000..4157a5b9 --- /dev/null +++ b/src/hooks/queries/auth/useRefreshToken.ts @@ -0,0 +1,27 @@ +import { useMutation } from "@tanstack/react-query"; +import axios from "axios"; + +export const useRefreshToken = () => { + const refreshMutation = useMutation({ + mutationFn: async () => { + try { + const response = await axios.post("/api/auth/refresh"); + return response.data; + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 400) { + return null; + } + throw error; + } + }, + onError: (error: Error) => { + console.error("Token refresh failed:", error); + }, + }); + + return { + refresh: () => refreshMutation.mutate(), + isPending: refreshMutation.isPending, + error: refreshMutation.error, + }; +}; diff --git a/src/hooks/queries/auth/useSignup.ts b/src/hooks/queries/auth/useSignup.ts new file mode 100644 index 00000000..d9d3b8d4 --- /dev/null +++ b/src/hooks/queries/auth/useSignup.ts @@ -0,0 +1,39 @@ +import { SignupSchema } from "@/schemas/authSchema"; +import { AuthResponse } from "@/types/response/auth"; +import { useMutation } from "@tanstack/react-query"; +import axios from "axios"; +import { useRouter } from "next/navigation"; +import { toast } from "react-hot-toast"; + +export const useSignup = () => { + const router = useRouter(); + + const signupMutation = useMutation({ + mutationFn: async (data: SignupSchema) => { + try { + const response = await axios.post("/api/auth/signup", data, { + withCredentials: false, + }); + return response.data; + } catch (error) { + if (axios.isAxiosError(error)) { + throw new Error(error.response?.data?.message || "회원가입 중 오류가 발생했습니다."); + } + throw error; + } + }, + onSuccess: () => { + toast.success("회원가입이 완료되었습니다!"); + router.push("/login"); + }, + onError: (error: Error) => { + toast.error(error.message); + }, + }); + + return { + signup: (data: SignupSchema) => signupMutation.mutate(data), + isPending: signupMutation.isPending, + error: signupMutation.error, + }; +}; diff --git a/src/hooks/queries/user/me/useMyApplications.ts b/src/hooks/queries/user/me/useMyApplications.ts new file mode 100644 index 00000000..468c3a9e --- /dev/null +++ b/src/hooks/queries/user/me/useMyApplications.ts @@ -0,0 +1,29 @@ +import { useInfiniteQuery } from "@tanstack/react-query"; +import axios from "axios"; +import { MyApplicationListResponse } from "@/types/response/user"; + +export const useMyApplications = (params?: { cursor?: string; limit?: number; status?: string; keyword?: string }) => { + const query = useInfiniteQuery({ + queryKey: ["myApplications", params], + queryFn: async ({ pageParam = undefined }) => { + const response = await axios.get("/api/users/me/applications", { + params: { + ...params, + cursor: pageParam, + }, + withCredentials: true, + }); + return response.data; + }, + getNextPageParam: (lastPage) => lastPage.nextCursor, + initialPageParam: undefined, + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 30, + }); + + return { + ...query, + isPending: query.isPending, + error: query.error, + }; +}; diff --git a/src/hooks/queries/user/me/useMyComments.ts b/src/hooks/queries/user/me/useMyComments.ts new file mode 100644 index 00000000..7cce63cd --- /dev/null +++ b/src/hooks/queries/user/me/useMyComments.ts @@ -0,0 +1,24 @@ +import { useQuery } from "@tanstack/react-query"; +import axios from "axios"; +import { MyCommentListResponse } from "@/types/response/user"; + +export const useMyComments = (params?: { page?: number; pageSize?: number }) => { + const query = useQuery({ + queryKey: ["myComments", params], + queryFn: async () => { + const response = await axios.get("/api/users/me/comments", { + params, + withCredentials: true, + }); + return response.data; + }, + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 30, + }); + + return { + ...query, + isPending: query.isPending, + error: query.error, + }; +}; diff --git a/src/hooks/queries/user/me/useMyForms.ts b/src/hooks/queries/user/me/useMyForms.ts new file mode 100644 index 00000000..efa98fc7 --- /dev/null +++ b/src/hooks/queries/user/me/useMyForms.ts @@ -0,0 +1,36 @@ +import { useInfiniteQuery } from "@tanstack/react-query"; +import axios from "axios"; +import { MyFormListResponse } from "@/types/response/user"; + +export const useMyForms = (params?: { + cursor?: string; + limit?: number; + orderBy?: string; + keyword?: string; + isPublic?: boolean; + isRecruiting?: boolean; +}) => { + const query = useInfiniteQuery({ + queryKey: ["myForms", params], + queryFn: async ({ pageParam = undefined }) => { + const response = await axios.get("/api/users/me/forms", { + params: { + ...params, + cursor: pageParam, + }, + withCredentials: true, + }); + return response.data; + }, + getNextPageParam: (lastPage) => lastPage.nextCursor, + initialPageParam: undefined, + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 30, + }); + + return { + ...query, + isPending: query.isPending, + error: query.error, + }; +}; diff --git a/src/hooks/queries/user/me/useMyPosts.ts b/src/hooks/queries/user/me/useMyPosts.ts new file mode 100644 index 00000000..4877708e --- /dev/null +++ b/src/hooks/queries/user/me/useMyPosts.ts @@ -0,0 +1,29 @@ +import { useInfiniteQuery } from "@tanstack/react-query"; +import axios from "axios"; +import { MyPostListResponse } from "@/types/response/user"; + +export const useMyPosts = (params?: { cursor?: string; limit?: number; orderBy?: string }) => { + const query = useInfiniteQuery({ + queryKey: ["myPosts", params], + queryFn: async ({ pageParam = undefined }) => { + const response = await axios.get("/api/users/me/posts", { + params: { + ...params, + cursor: pageParam, + }, + withCredentials: true, + }); + return response.data; + }, + getNextPageParam: (lastPage) => lastPage.nextCursor, + initialPageParam: undefined, + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 30, + }); + + return { + ...query, + isPending: query.isPending, + error: query.error, + }; +}; diff --git a/src/hooks/queries/user/me/useMyScraps.ts b/src/hooks/queries/user/me/useMyScraps.ts new file mode 100644 index 00000000..4e8ec5fc --- /dev/null +++ b/src/hooks/queries/user/me/useMyScraps.ts @@ -0,0 +1,35 @@ +import { useInfiniteQuery } from "@tanstack/react-query"; +import axios from "axios"; +import { MyFormListResponse } from "@/types/response/user"; + +export const useMyScraps = (params?: { + cursor?: string; + limit?: number; + orderBy?: string; + isPublic?: boolean; + isRecruiting?: boolean; +}) => { + const query = useInfiniteQuery({ + queryKey: ["myScraps", params], + queryFn: async ({ pageParam = undefined }) => { + const response = await axios.get("/api/users/me/scrap", { + params: { + ...params, + cursor: pageParam, + }, + withCredentials: true, + }); + return response.data; + }, + getNextPageParam: (lastPage) => lastPage.nextCursor, + initialPageParam: undefined, + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 30, + }); + + return { + ...query, + isPending: query.isPending, + error: query.error, + }; +}; diff --git a/src/hooks/queries/user/me/usePassword.ts b/src/hooks/queries/user/me/usePassword.ts new file mode 100644 index 00000000..85f95141 --- /dev/null +++ b/src/hooks/queries/user/me/usePassword.ts @@ -0,0 +1,39 @@ +import axios from "axios"; +import toast from "react-hot-toast"; +import { useMutation } from "@tanstack/react-query"; + +interface PasswordData { + currentPassword: string; + newPassword: string; +} + +export const usePassword = () => { + const mutation = useMutation({ + mutationFn: async (data: PasswordData) => { + const response = await axios.patch("/api/users/me/password", data); + return response.data; + }, + onSuccess: () => { + toast.success("비밀번호가 변경되었습니다!\n다시 로그인해주세요.", { + style: { + whiteSpace: "pre-line", + textAlign: "center", + }, + }); + }, + onError: (error) => { + if (axios.isAxiosError(error)) { + const errorMessage = error.response?.data?.message || "비밀번호 변경에 실패했습니다."; + toast.error(errorMessage); + } else { + toast.error("비밀번호 변경 중 오류가 발생했습니다."); + } + }, + }); + + return { + ...mutation, + isPending: mutation.isPending, + error: mutation.error, + }; +}; diff --git a/src/hooks/queries/user/me/useUser.ts b/src/hooks/queries/user/me/useUser.ts new file mode 100644 index 00000000..1bcf4deb --- /dev/null +++ b/src/hooks/queries/user/me/useUser.ts @@ -0,0 +1,124 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import axios from "axios"; +import { UserResponse } from "@/types/response/user"; +import toast from "react-hot-toast"; + +interface UpdateProfileData { + name?: string; + nickname?: string; + phoneNumber?: string; + imageUrl?: string; + storeName?: string; + storePhoneNumber?: string; + location?: string; +} + +export const useUser = () => { + const queryClient = useQueryClient(); + + const userQuery = useQuery<{ user: UserResponse | null }>({ + queryKey: ["user"], + queryFn: async () => { + try { + const response = await axios.get("/api/users/me", { + withCredentials: true, + }); + + const userData = { + user: response.data, + }; + + return userData; + } catch (error) { + if (axios.isAxiosError(error)) { + if (error.response?.status === 401) { + return { user: null }; + } else { + toast.error("사용자 정보를 불러오는 데 실패했습니다." + error.response?.data.message); + } + } + throw error; + } + }, + retry: false, + staleTime: 0, + gcTime: 1000 * 60 * 30, + }); + + const updateProfileMutation = useMutation({ + mutationFn: async (data: UpdateProfileData) => { + const response = await axios.patch("/api/users/me", data); + return response.data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["user"] }); + toast.success("프로필이 성공적으로 수정되었습니다."); + }, + onError: (error) => { + if (axios.isAxiosError(error)) { + const errorMessage = error.response?.data?.message || "프로필 수정에 실패했습니다."; + console.error("Profile update error:", { + status: error.response?.status, + data: error.response?.data, + }); + toast.error(errorMessage); + } else { + console.error("Unexpected error:", error); + toast.error("프로필 수정 중 오류가 발생했습니다."); + } + }, + }); + + const uploadImageMutation = useMutation({ + mutationFn: async (file: File) => { + const formData = new FormData(); + formData.append("image", file); + + const response = await axios.post("/api/images/upload", formData, { + withCredentials: true, + }); + return response.data; + }, + onError: (error) => { + if (axios.isAxiosError(error)) { + const errorMessage = error.response?.data?.message || "이미지 업로드에 실패했습니다."; + toast.error(errorMessage); + } else { + toast.error("이미지 업로드 중 오류가 발생했습니다."); + } + }, + }); + + const updateProfile = async (data: UpdateProfileData, imageFile?: File | null) => { + try { + let imageUrl = data.imageUrl; + + if (imageFile) { + const uploadResponse = await uploadImageMutation.mutateAsync(imageFile); + if (uploadResponse?.url) { + imageUrl = uploadResponse.url; + } + } + + await updateProfileMutation.mutateAsync({ + ...data, + imageUrl, + }); + + return true; + } catch (error) { + console.error(error); + return false; + } + }; + + return { + user: userQuery.data?.user || null, + refetch: userQuery.refetch, + isLoading: userQuery.isLoading, + error: userQuery.error, + isPending: userQuery.isPending, + updateProfile, + isUpdating: updateProfileMutation.isPending || uploadImageMutation.isPending, + }; +}; diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts deleted file mode 100644 index 59f2810b..00000000 --- a/src/hooks/useAuth.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { LoginSchema, SignupSchema } from "@/schemas/authSchema"; -import { AuthResponse } from "@/types/response/auth"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import axios from "axios"; -import { useRouter } from "next/navigation"; -import { toast } from "react-hot-toast"; - -export const useAuth = () => { - const router = useRouter(); - const queryClient = useQueryClient(); - - // 회원가입 mutation - const signupMutation = useMutation({ - mutationFn: async (data: SignupSchema) => { - try { - const response = await axios.post("/api/auth/signup", data, { - withCredentials: false, - }); - return response.data; - } catch (error) { - if (axios.isAxiosError(error)) { - throw new Error(error.response?.data?.message || "회원가입 중 오류가 발생했습니다."); - } - throw error; - } - }, - onSuccess: () => { - toast.success("회원가입이 완료되었습니다!"); - router.push("/login"); - }, - onError: (error: Error) => { - toast.error(error.message); - }, - }); - - // 로그인 mutation - const loginMutation = useMutation({ - mutationFn: async (data: LoginSchema) => { - try { - const response = await axios.post("/api/auth/login", data, { - withCredentials: false, - }); - return response.data; - } catch (error) { - if (axios.isAxiosError(error)) { - throw new Error(error.response?.data?.message || "로그인 중 오류가 발생했습니다."); - } - throw error; - } - }, - onSuccess: (data) => { - if (data?.user) { - // React Query 캐시 업데이트 - queryClient.setQueryData(["user"], { user: data.user }); - - toast.success("로그인되었습니다!"); - router.push("/"); - router.refresh(); - } - }, - onError: (error: Error) => { - toast.error(error.message); - }, - }); - - // 로그아웃 mutation - const logoutMutation = useMutation({ - mutationFn: async () => { - const response = await axios.post("/api/auth/logout"); - return response.data; - }, - onSuccess: () => { - queryClient.clear(); - router.push("/"); - router.refresh(); - }, - onError: (error: Error) => { - toast.error(error.message || "로그아웃 중 오류가 발생했습니다."); - }, - }); - - // 토큰 갱신 mutation - const refreshMutation = useMutation({ - mutationFn: async () => { - try { - const response = await axios.post("/api/auth/refresh"); - return response.data; - } catch (error) { - if (axios.isAxiosError(error) && error.response?.status === 400) { - return null; - } - throw error; - } - }, - onError: (error: Error) => { - console.error("Token refresh failed:", error); - }, - }); - - const signup = (data: SignupSchema) => signupMutation.mutate(data); - const login = (data: LoginSchema) => loginMutation.mutate(data); - const logout = () => logoutMutation.mutate(); - const refresh = async () => { - try { - await refreshMutation.mutateAsync(); - return true; - } catch { - return false; - } - }; - - return { - signup, - login, - logout, - refresh, - isSignupPending: signupMutation.isPending, - isLoginPending: loginMutation.isPending, - isLogoutPending: logoutMutation.isPending, - isRefreshPending: refreshMutation.isPending, - signupError: signupMutation.error, - loginError: loginMutation.error, - logoutError: logoutMutation.error, - refreshError: refreshMutation.error, - }; -}; diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts deleted file mode 100644 index a0de5be9..00000000 --- a/src/hooks/useUser.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { useQuery, useInfiniteQuery } from "@tanstack/react-query"; -import axios from "axios"; -import { - UserResponse, - MyFormListResponse, - MyApplicationListResponse, - MyPostListResponse, - MyCommentListResponse, -} from "@/types/response/user"; -import toast from "react-hot-toast"; - -export const useUser = () => { - // 사용자 정보 조회 - const userQuery = useQuery<{ user: UserResponse | null }>({ - queryKey: ["user"], - queryFn: async () => { - try { - const response = await axios.get("/api/users/me", { - withCredentials: true, - }); - - // 응답 데이터 구조 수정 - const userData = { - user: response.data, // response.data가 이미 UserResponse 형태라고 가정 - }; - - return userData; - } catch (error) { - if (axios.isAxiosError(error)) { - if (error.response?.status === 401) { - return { user: null }; - } else { - toast.error("사용자 정보를 불러오는 데 실패했습니다." + error.response?.data.message); - } - } - throw error; - } - }, - retry: false, - staleTime: 0, - gcTime: 1000 * 60 * 30, - }); - - // 내가 생성한 알바폼 목록 조회 (무한 스크롤) - const useMyForms = (params?: { - cursor?: string; // 다음 페이지 커서 - limit?: number; // 한 페이지당 아이템 수 - orderBy?: string; // 정렬 기준 - keyword?: string; // 검색어 - isPublic?: boolean; // 공개 여부 - isRecruiting?: boolean; // 모집 중 여부 - }) => { - return useInfiniteQuery({ - queryKey: ["myForms", params], - queryFn: async ({ pageParam = undefined }) => { - const response = await axios.get("/api/users/me/forms", { - params: { - ...params, - cursor: pageParam, - }, - withCredentials: true, - }); - return response.data; - }, - getNextPageParam: (lastPage) => lastPage.nextCursor, // 다음 페이지 커서 반환 - initialPageParam: undefined, // 초기 페이지 파라미터 - staleTime: 1000 * 60 * 5, - gcTime: 1000 * 60 * 30, - }); - }; - - // 내가 지원한 알바폼 목록 조회 (무한 스크롤) - const useMyApplications = (params?: { - cursor?: string; // 다음 페이지 커서 - limit?: number; // 한 페이지당 아이템 수 - status?: string; // 지원 상태 필터 - keyword?: string; // 검색어 - }) => { - return useInfiniteQuery({ - queryKey: ["myApplications", params], - queryFn: async ({ pageParam = undefined }) => { - const response = await axios.get("/api/users/me/applications", { - params: { - ...params, - cursor: pageParam, - }, - withCredentials: true, - }); - return response.data; - }, - getNextPageParam: (lastPage) => lastPage.nextCursor, - initialPageParam: undefined, - staleTime: 1000 * 60 * 5, - gcTime: 1000 * 60 * 30, - }); - }; - - // 내가 작성한 게시글 목록 조회 (무한 스크롤) - const useMyPosts = (params?: { - cursor?: string; // 다음 페이지 커서 - limit?: number; // 한 페이지당 아이템 수 - orderBy?: string; // 정렬 기준 - }) => { - return useInfiniteQuery({ - queryKey: ["myPosts", params], - queryFn: async ({ pageParam = undefined }) => { - const response = await axios.get("/api/users/me/posts", { - params: { - ...params, - cursor: pageParam, - }, - withCredentials: true, - }); - return response.data; - }, - getNextPageParam: (lastPage) => lastPage.nextCursor, - initialPageParam: undefined, - staleTime: 1000 * 60 * 5, - gcTime: 1000 * 60 * 30, - }); - }; - - // 내가 작성한 댓글 목록 조회 (페이지네이션) - const useMyComments = (params?: { - page?: number; // 페이지 번호 - pageSize?: number; // 한 페이지당 아이템 수 - }) => { - return useQuery({ - queryKey: ["myComments", params], - queryFn: async () => { - const response = await axios.get("/api/users/me/comments", { - params, - withCredentials: true, - }); - return response.data; - }, - staleTime: 1000 * 60 * 5, - gcTime: 1000 * 60 * 30, - }); - }; - - // 내가 스크랩한 알바폼 목록 조회 (무한 스크롤) - const useMyScraps = (params?: { - cursor?: string; // 다음 페이지 커서 - limit?: number; // 한 페이지당 아이템 수 - orderBy?: string; // 정렬 기준 - isPublic?: boolean; // 공개 여부 - isRecruiting?: boolean; // 모집 중 여부 - }) => { - return useInfiniteQuery({ - queryKey: ["myScraps", params], - queryFn: async ({ pageParam = undefined }) => { - const response = await axios.get("/api/users/me/scrap", { - params: { - ...params, - cursor: pageParam, - }, - withCredentials: true, - }); - return response.data; - }, - getNextPageParam: (lastPage) => lastPage.nextCursor, - initialPageParam: undefined, - staleTime: 1000 * 60 * 5, - gcTime: 1000 * 60 * 30, - }); - }; - - // 훅의 반환값들 - return { - user: userQuery.data?.user || null, - refetch: userQuery.refetch, - isLoading: userQuery.isLoading, - error: userQuery.error, - useMyForms, - useMyApplications, - useMyPosts, - useMyComments, - useMyScraps, - }; -};