diff --git a/src/app/(pages)/mypage/components/FilterBar/TabMenu.tsx b/src/app/(pages)/mypage/components/FilterBar/TabMenu.tsx index 036ed539..55c49764 100644 --- a/src/app/(pages)/mypage/components/FilterBar/TabMenu.tsx +++ b/src/app/(pages)/mypage/components/FilterBar/TabMenu.tsx @@ -13,6 +13,12 @@ export default function TabMenu() { const createQueryString = (tab: string) => { const params = new URLSearchParams(searchParams.toString()); params.set("tab", tab); + + if (tab !== "scrap") { + params.delete("isPublic"); + params.delete("isRecruiting"); + } + return params.toString(); }; diff --git a/src/app/(pages)/mypage/components/sections/ScrapsSection.tsx b/src/app/(pages)/mypage/components/sections/ScrapsSection.tsx index 6b4575e7..32690eb3 100644 --- a/src/app/(pages)/mypage/components/sections/ScrapsSection.tsx +++ b/src/app/(pages)/mypage/components/sections/ScrapsSection.tsx @@ -4,56 +4,97 @@ import React, { useEffect } from "react"; import { useInView } from "react-intersection-observer"; import { useMyScraps } from "@/hooks/queries/user/me/useMyScraps"; import { useSortStore } from "@/store/sortStore"; -import { useFilterStore } from "@/store/filterStore"; import type { FormListType } from "@/types/response/form"; import FilterDropdown from "@/app/components/button/dropdown/FilterDropdown"; import { filterPublicOptions, filterRecruitingOptions } from "@/constants/filterOptions"; +import { useRouter, usePathname, useSearchParams } from "next/navigation"; -// 한 페이지당 스크랩 수 const SCRAPS_PER_PAGE = 10; export default function ScrapsSection() { - // 정렬 상태 관리 + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + + // URL 쿼리 파라미터에서 필터 상태 가져오기 + const isPublic = searchParams.get("isPublic"); + const isRecruiting = searchParams.get("isRecruiting"); const { orderBy } = useSortStore(); - const { filterBy, setFilterBy } = useFilterStore(); + + // 초기 마운트 시 필터 값 설정 + useEffect(() => { + const params = new URLSearchParams(searchParams); + let needsUpdate = false; + + if (!params.has("isPublic")) { + params.set("isPublic", "true"); + needsUpdate = true; + } + if (!params.has("isRecruiting")) { + params.set("isRecruiting", "true"); + needsUpdate = true; + } + if (needsUpdate) { + params.set("tab", "scrap"); + router.push(`${pathname}?${params.toString()}`); + } + }, []); // 무한 스크롤을 위한 Intersection Observer 설정 const { ref, inView } = useInView({ - threshold: 0.1, // 10% 정도 보이면 트리거 - triggerOnce: true, // 한 번만 트리거 (불필요한 API 호출 방지) - rootMargin: "100px", // 하단 100px 전에 미리 로드 + threshold: 0.1, + triggerOnce: true, + rootMargin: "100px", }); - // 내가 스크랩한 알바폼 목록 조회 + // 내가 스크랩한 알폼 목록 조회 const { data, isLoading, error, hasNextPage, fetchNextPage, isFetchingNextPage } = useMyScraps({ limit: SCRAPS_PER_PAGE, orderBy: orderBy.scrap, - isPublic: filterBy.isPublic, - isRecruiting: filterBy.isRecruiting, + isPublic: isPublic === "true" ? true : isPublic === "false" ? false : undefined, + isRecruiting: isRecruiting === "true" ? true : isRecruiting === "false" ? false : undefined, }); + // 공개 여부 필터 변경 함수 const handlePublicFilter = (selected: string) => { const option = filterPublicOptions.find((opt) => opt.label === selected); if (option) { - setFilterBy("isPublic", String(option.value)); + const params = new URLSearchParams(searchParams); + if (selected === "전체") { + params.delete("isPublic"); + } else { + params.set("isPublic", String(option.value)); + } + params.set("tab", "scrap"); + router.push(`${pathname}?${params.toString()}`); } }; + // 모집 여부 필터 변경 함수 const handleRecruitingFilter = (selected: string) => { const option = filterRecruitingOptions.find((opt) => opt.label === selected); if (option) { - setFilterBy("isRecruiting", String(option.value)); + const params = new URLSearchParams(searchParams); + if (selected === "전체") { + params.delete("isRecruiting"); + } else { + params.set("isRecruiting", String(option.value)); + } + params.set("tab", "scrap"); + router.push(`${pathname}?${params.toString()}`); } }; // 현재 필터 상태에 따른 초기값 설정을 위한 함수들 - const getInitialPublicValue = (isPublic: boolean) => { - const option = filterPublicOptions.find((opt) => opt.value === isPublic); + const getInitialPublicValue = (isPublic: string | null) => { + if (!isPublic) return "전체"; + const option = filterPublicOptions.find((opt) => String(opt.value) === isPublic); return option?.label || "전체"; }; - const getInitialRecruitingValue = (isRecruiting: boolean) => { - const option = filterRecruitingOptions.find((opt) => opt.value === isRecruiting); + const getInitialRecruitingValue = (isRecruiting: string | null) => { + if (!isRecruiting) return "전체"; + const option = filterRecruitingOptions.find((opt) => String(opt.value) === isRecruiting); return option?.label || "전체"; }; @@ -64,7 +105,7 @@ export default function ScrapsSection() { } }, [inView, hasNextPage, fetchNextPage, isFetchingNextPage]); - // 에러 ��태 처리 + // 에러 상태 처리 if (error) { return (
@@ -89,26 +130,26 @@ export default function ScrapsSection() {
option.label)} - initialValue={getInitialPublicValue(filterBy.isPublic)} + initialValue={getInitialPublicValue(isPublic)} onChange={handlePublicFilter} /> option.label)} - initialValue={getInitialRecruitingValue(filterBy.isRecruiting)} + initialValue={getInitialRecruitingValue(isRecruiting)} onChange={handleRecruitingFilter} />
{/* 스크랩 목록 렌더링 */} - {!data?.pages[0]?.data?.length ? ( + {!data?.pages?.[0]?.data?.length ? (

스크랩한 공고가 없습니다.

) : ( <> - {data.pages.map((page, index) => ( - + {data?.pages.map((page) => ( + {page.data.map((scrap: FormListType) => (

{scrap.title}

diff --git a/src/app/api/forms/[formId]/route.ts b/src/app/api/forms/[formId]/route.ts index f4d7b3f5..2db90a18 100644 --- a/src/app/api/forms/[formId]/route.ts +++ b/src/app/api/forms/[formId]/route.ts @@ -3,20 +3,10 @@ import { cookies } from "next/headers"; import { NextRequest, NextResponse } from "next/server"; import apiClient from "@/lib/apiClient"; -// 알바폼 상세 조회 +// 알바폼 상세 조회(로그인 안한 유저도 조회 가능) export async function GET(req: NextRequest, { params }: { params: { formId: string } }) { try { - const accessToken = cookies().get("accessToken")?.value; - - if (!accessToken) { - return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); - } - - const response = await apiClient.get(`/forms/${params.formId}`, { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }); + const response = await apiClient.get(`/forms/${params.formId}`); return NextResponse.json(response.data); } catch (error: unknown) { diff --git a/src/app/api/forms/route.ts b/src/app/api/forms/route.ts index e8d55145..73744db2 100644 --- a/src/app/api/forms/route.ts +++ b/src/app/api/forms/route.ts @@ -2,6 +2,7 @@ import { AxiosError } from "axios"; import { cookies } from "next/headers"; import { NextRequest, NextResponse } from "next/server"; import apiClient from "@/lib/apiClient"; +import { cleanedParameters } from "@/utils/cleanedParameters"; // 알바폼 생성 export async function POST(req: NextRequest) { @@ -44,15 +45,18 @@ export async function GET(req: NextRequest) { const { searchParams } = new URL(req.url); const params = { - page: searchParams.get("page"), + cursor: searchParams.get("cursor"), limit: searchParams.get("limit"), + orderBy: searchParams.get("orderBy"), + keyword: searchParams.get("keyword"), + isRecruiting: searchParams.get("isRecruiting"), }; + // null, undefined, 빈 문자열을 가진 파라미터 제거 + const cleanedParams = cleanedParameters(params); + const response = await apiClient.get("/forms", { - params, - headers: { - Authorization: `Bearer ${accessToken}`, - }, + params: cleanedParams, }); return NextResponse.json(response.data); diff --git a/src/app/api/posts/route.ts b/src/app/api/posts/route.ts index ce0a9e3a..20f20a2c 100644 --- a/src/app/api/posts/route.ts +++ b/src/app/api/posts/route.ts @@ -2,6 +2,7 @@ import { AxiosError } from "axios"; import { cookies } from "next/headers"; import { NextResponse } from "next/server"; import apiClient from "@/lib/apiClient"; +import { cleanedParameters } from "@/utils/cleanedParameters"; // 게시글 목록 조회 API export async function GET(request: Request) { @@ -11,14 +12,16 @@ export async function GET(request: Request) { const params = { cursor: searchParams.get("cursor"), // 페이지네이션 커서 limit: searchParams.get("limit"), // 한 페이지당 항목 수 - keyword: searchParams.get("keyword"), // 검색 키워드 orderBy: searchParams.get("orderBy"), // 정렬 기준 - category: searchParams.get("category"), // 카테고리 + keyword: searchParams.get("keyword"), // 검색 키워드 }; + // null, undefined, 빈 문자열을 가진 파라미터 제거 + const cleanedParams = cleanedParameters(params); + // 게시글 목록 조회 요청 const response = await apiClient.get("/posts", { - params, + params: cleanedParams, }); return NextResponse.json(response.data); diff --git a/src/app/api/users/me/applications/route.ts b/src/app/api/users/me/applications/route.ts index cb602554..f8043632 100644 --- a/src/app/api/users/me/applications/route.ts +++ b/src/app/api/users/me/applications/route.ts @@ -2,6 +2,7 @@ import { AxiosError } from "axios"; import { cookies } from "next/headers"; import { NextResponse } from "next/server"; import apiClient from "@/lib/apiClient"; +import { cleanedParameters } from "@/utils/cleanedParameters"; // 내가 지원한 알바폼 목록 조회 API export async function GET(request: Request) { @@ -22,12 +23,15 @@ export async function GET(request: Request) { keyword: searchParams.get("keyword"), // 검색 키워드 }; + // null, undefined, 빈 문자열을 가진 파라미터 제거 + const cleanedParams = cleanedParameters(params); + // 지원 목록 조회 요청 const response = await apiClient.get("/users/me/applications", { headers: { Authorization: `Bearer ${accessToken}`, }, - params, + params: cleanedParams, }); return NextResponse.json(response.data); diff --git a/src/app/api/users/me/comments/route.ts b/src/app/api/users/me/comments/route.ts index dece6807..5f082823 100644 --- a/src/app/api/users/me/comments/route.ts +++ b/src/app/api/users/me/comments/route.ts @@ -2,6 +2,7 @@ import { AxiosError } from "axios"; import { cookies } from "next/headers"; import { NextResponse } from "next/server"; import apiClient from "@/lib/apiClient"; +import { cleanedParameters } from "@/utils/cleanedParameters"; // 내가 작성한 댓글 목록 조회 API export async function GET(request: Request) { @@ -16,18 +17,19 @@ export async function GET(request: Request) { // URL 쿼리 파라미터 파싱 const { searchParams } = new URL(request.url); const params = { - cursor: searchParams.get("cursor"), // 페이지네이션 커서 - limit: searchParams.get("limit"), // 한 페이지당 항목 수 - keyword: searchParams.get("keyword"), // 검색 키워드 - orderBy: searchParams.get("orderBy"), // 정렬 기준 + page: searchParams.get("page"), // 페이지네이션 커서 + pageSize: searchParams.get("pageSize"), // 한 페이지당 항목 수 }; + // null, undefined, 빈 문자열을 가진 파라미터 제거 + const cleanedParams = cleanedParameters(params); + // 내가 작성한 댓글 목록 조회 요청 const response = await apiClient.get("/users/me/comments", { headers: { Authorization: `Bearer ${accessToken}`, }, - params, + params: cleanedParams, }); return NextResponse.json(response.data); diff --git a/src/app/api/users/me/forms/route.ts b/src/app/api/users/me/forms/route.ts index 4f602889..9e7d2d89 100644 --- a/src/app/api/users/me/forms/route.ts +++ b/src/app/api/users/me/forms/route.ts @@ -2,6 +2,7 @@ import { AxiosError } from "axios"; import { cookies } from "next/headers"; import { NextResponse } from "next/server"; import apiClient from "@/lib/apiClient"; +import { cleanedParameters } from "@/utils/cleanedParameters"; // 내가 생성한 알바폼 목록 조회 API export async function GET(request: Request) { @@ -24,12 +25,15 @@ export async function GET(request: Request) { isRecruiting: searchParams.get("isRecruiting"), // 모집 중 여부 }; + // null, undefined, 빈 문자열을 가진 파라미터 제거 + const cleanedParams = cleanedParameters(params); + // 알바폼 목록 조회 요청 const response = await apiClient.get("/users/me/forms", { headers: { Authorization: `Bearer ${accessToken}`, }, - params, + params: cleanedParams, }); return NextResponse.json(response.data); diff --git a/src/app/api/users/me/posts/route.ts b/src/app/api/users/me/posts/route.ts index cc5c2432..0f615d2b 100644 --- a/src/app/api/users/me/posts/route.ts +++ b/src/app/api/users/me/posts/route.ts @@ -2,6 +2,7 @@ import { AxiosError } from "axios"; import { cookies } from "next/headers"; import { NextResponse } from "next/server"; import apiClient from "@/lib/apiClient"; +import { cleanedParameters } from "@/utils/cleanedParameters"; // 내가 작성한 게시글 목록 조회 API export async function GET(request: Request) { @@ -18,17 +19,18 @@ export async function GET(request: Request) { const params = { cursor: searchParams.get("cursor"), // 페이지네이션 커서 limit: searchParams.get("limit"), // 한 페이지당 항목 수 - keyword: searchParams.get("keyword"), // 검색 키워드 - orderBy: searchParams.get("orderBy"), // 정렬 기준 (최신순, 좋아요순 등) - category: searchParams.get("category"), // 게시글 카테고리 + orderBy: searchParams.get("orderBy"), // 정렬 기준 (최신순, 댓글수 순, 좋아요순 등) }; + // null, undefined, 빈 문자열을 가진 파라미터 제거 + const cleanedParams = cleanedParameters(params); + // 내가 작성한 게시글 목록 조회 요청 const response = await apiClient.get("/users/me/posts", { headers: { Authorization: `Bearer ${accessToken}`, }, - params, + params: cleanedParams, }); return NextResponse.json(response.data); diff --git a/src/app/api/users/me/scrap/route.ts b/src/app/api/users/me/scrap/route.ts index 1879b26a..3aa0a8f9 100644 --- a/src/app/api/users/me/scrap/route.ts +++ b/src/app/api/users/me/scrap/route.ts @@ -2,6 +2,7 @@ import { AxiosError } from "axios"; import { cookies } from "next/headers"; import { NextResponse } from "next/server"; import apiClient from "@/lib/apiClient"; +import { cleanedParameters } from "@/utils/cleanedParameters"; // 내가 스크랩한 알바폼 목록 조회 API export async function GET(request: Request) { @@ -19,26 +20,19 @@ export async function GET(request: Request) { cursor: searchParams.get("cursor"), limit: searchParams.get("limit"), orderBy: searchParams.get("orderBy"), + isPublic: searchParams.get("isPublic"), + isRecruiting: searchParams.get("isRecruiting"), }; - // isPublic과 isRecruiting은 값이 있을 때만 추가 - const isPublic = searchParams.get("isPublic"); - const isRecruiting = searchParams.get("isRecruiting"); - - if (isPublic !== null && isPublic !== "null") { - params.isPublic = isPublic; - } - - if (isRecruiting !== null && isRecruiting !== "null") { - params.isRecruiting = isRecruiting; - } + // null, undefined, 빈 문자열을 가진 파라미터 제거 + const cleanedParams = cleanedParameters(params); // 스크랩 목록 조회 요청 const response = await apiClient.get("/users/me/scrap", { headers: { Authorization: `Bearer ${accessToken}`, }, - params, + params: cleanedParams, }); return NextResponse.json(response.data); diff --git a/src/app/components/modal/modals/form/ChangePasswordModal.tsx b/src/app/components/modal/modals/form/ChangePasswordModal.tsx index f5c5b6f1..aca537f3 100644 --- a/src/app/components/modal/modals/form/ChangePasswordModal.tsx +++ b/src/app/components/modal/modals/form/ChangePasswordModal.tsx @@ -5,7 +5,7 @@ import { z } from "zod"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useLogout } from "@/hooks/queries/auth/useLogout"; -import { usePassword } from "@/hooks/queries/user/me/usePassword"; +import { usePassword } from "@/hooks/queries/user/me/useChangePassword"; interface ChangePasswordModalProps { isOpen: boolean; diff --git a/src/app/components/modal/modals/form/EditMyProfileModal.tsx b/src/app/components/modal/modals/form/EditMyProfileModal.tsx index ea2f2bb3..012d8957 100644 --- a/src/app/components/modal/modals/form/EditMyProfileModal.tsx +++ b/src/app/components/modal/modals/form/EditMyProfileModal.tsx @@ -6,6 +6,7 @@ import Image from "next/image"; import { FiUser, FiEdit2 } from "react-icons/fi"; import BaseInput from "@/app/components/input/text/BaseInput"; import { useUser } from "@/hooks/queries/user/me/useUser"; +import { useUpdateProfile } from "@/hooks/queries/user/me/useUpdateProfile"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; @@ -26,7 +27,8 @@ const editMyProfileSchema = z.object({ type EditMyProfileFormData = z.infer; const EditMyProfileModal = ({ isOpen, onClose, className }: EditMyProfileModalProps) => { - const { user, updateProfile, isUpdating } = useUser(); + const { user } = useUser(); + const { updateProfile, isUpdating } = useUpdateProfile(); const [selectedFile, setSelectedFile] = useState(null); const [previewUrl, setPreviewUrl] = useState(""); const fileInputRef = useRef(null); diff --git a/src/app/components/modal/modals/form/EditOwnerProfileModal.tsx b/src/app/components/modal/modals/form/EditOwnerProfileModal.tsx index 74575117..35cb8038 100644 --- a/src/app/components/modal/modals/form/EditOwnerProfileModal.tsx +++ b/src/app/components/modal/modals/form/EditOwnerProfileModal.tsx @@ -6,6 +6,7 @@ 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/queries/user/me/useUser"; +import { useUpdateProfile } from "@/hooks/queries/user/me/useUpdateProfile"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; @@ -37,7 +38,8 @@ type Field = { }; const EditOwnerProfileModal = ({ isOpen, onClose, className }: EditOwnerProfileModalProps) => { - const { user, updateProfile, isUpdating } = useUser(); + const { user } = useUser(); + const { updateProfile, isUpdating } = useUpdateProfile(); const [selectedFile, setSelectedFile] = useState(null); const [previewUrl, setPreviewUrl] = useState(""); const fileInputRef = useRef(null); diff --git a/src/hooks/queries/form/useForms.ts b/src/hooks/queries/form/useForms.ts new file mode 100644 index 00000000..34ee15e4 --- /dev/null +++ b/src/hooks/queries/form/useForms.ts @@ -0,0 +1,49 @@ +import { useInfiniteQuery } from "@tanstack/react-query"; +import axios from "axios"; +import type { FormListResponse } from "@/types/response/form"; +import toast from "react-hot-toast"; + +interface UseFormsParams { + cursor?: string; + limit?: number; + orderBy?: string; + keyword?: string; + isRecruiting?: boolean; +} + +export const useForms = ({ cursor, limit = 10, orderBy, keyword, isRecruiting }: UseFormsParams = {}) => { + const query = useInfiniteQuery({ + queryKey: ["forms", { limit, orderBy, keyword, isRecruiting }], + queryFn: async () => { + try { + const response = await axios.get("/api/forms", { + params: { + cursor, + limit, + orderBy, + keyword, + isRecruiting, + }, + withCredentials: false, + }); + return response.data; + } catch (error) { + if (axios.isAxiosError(error)) { + const errorMessage = error.response?.data?.message || "알바폼 목록을 불러오는데 실패했습니다."; + toast.error(errorMessage); + } + throw error; + } + }, + 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/useChangePassword.ts similarity index 100% rename from src/hooks/queries/user/me/usePassword.ts rename to src/hooks/queries/user/me/useChangePassword.ts diff --git a/src/hooks/queries/user/me/useMyApplications.ts b/src/hooks/queries/user/me/useMyApplications.ts index 468c3a9e..e4ebce02 100644 --- a/src/hooks/queries/user/me/useMyApplications.ts +++ b/src/hooks/queries/user/me/useMyApplications.ts @@ -2,14 +2,23 @@ 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 }) => { +interface UseMyApplicationsParams { + cursor?: string; + limit?: number; + status?: string; + keyword?: string; +} + +export const useMyApplications = ({ cursor, limit, status, keyword }: UseMyApplicationsParams = {}) => { const query = useInfiniteQuery({ - queryKey: ["myApplications", params], - queryFn: async ({ pageParam = undefined }) => { + queryKey: ["myApplications", { limit, status, keyword }], + queryFn: async () => { const response = await axios.get("/api/users/me/applications", { params: { - ...params, - cursor: pageParam, + cursor, + limit, + status, + keyword, }, withCredentials: true, }); diff --git a/src/hooks/queries/user/me/useMyForms.ts b/src/hooks/queries/user/me/useMyForms.ts index efa98fc7..1ee5cda5 100644 --- a/src/hooks/queries/user/me/useMyForms.ts +++ b/src/hooks/queries/user/me/useMyForms.ts @@ -2,21 +2,27 @@ import { useInfiniteQuery } from "@tanstack/react-query"; import axios from "axios"; import { MyFormListResponse } from "@/types/response/user"; -export const useMyForms = (params?: { +interface UseMyFormsParams { cursor?: string; limit?: number; orderBy?: string; keyword?: string; isPublic?: boolean; isRecruiting?: boolean; -}) => { +} + +export const useMyForms = ({ cursor, limit, orderBy, keyword, isPublic, isRecruiting }: UseMyFormsParams = {}) => { const query = useInfiniteQuery({ - queryKey: ["myForms", params], - queryFn: async ({ pageParam = undefined }) => { + queryKey: ["myForms", { limit, orderBy, keyword, isPublic, isRecruiting }], + queryFn: async () => { const response = await axios.get("/api/users/me/forms", { params: { - ...params, - cursor: pageParam, + cursor, + limit, + orderBy, + keyword, + isPublic, + isRecruiting, }, withCredentials: true, }); diff --git a/src/hooks/queries/user/me/useMyPosts.ts b/src/hooks/queries/user/me/useMyPosts.ts index 4877708e..ed9de95f 100644 --- a/src/hooks/queries/user/me/useMyPosts.ts +++ b/src/hooks/queries/user/me/useMyPosts.ts @@ -2,14 +2,21 @@ 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 }) => { +interface UseMyPostsParams { + cursor?: string; + limit?: number; + orderBy?: string; +} + +export const useMyPosts = ({ cursor, limit, orderBy }: UseMyPostsParams = {}) => { const query = useInfiniteQuery({ - queryKey: ["myPosts", params], - queryFn: async ({ pageParam = undefined }) => { + queryKey: ["myPosts", { limit, orderBy }], + queryFn: async () => { const response = await axios.get("/api/users/me/posts", { params: { - ...params, - cursor: pageParam, + cursor, + limit, + orderBy, }, withCredentials: true, }); diff --git a/src/hooks/queries/user/me/useMyScraps.ts b/src/hooks/queries/user/me/useMyScraps.ts index 4e8ec5fc..e9a611c3 100644 --- a/src/hooks/queries/user/me/useMyScraps.ts +++ b/src/hooks/queries/user/me/useMyScraps.ts @@ -2,34 +2,41 @@ import { useInfiniteQuery } from "@tanstack/react-query"; import axios from "axios"; import { MyFormListResponse } from "@/types/response/user"; -export const useMyScraps = (params?: { +interface UseMyScrapsParams { cursor?: string; limit?: number; orderBy?: string; isPublic?: boolean; isRecruiting?: boolean; -}) => { +} + +export const useMyScraps = ({ cursor, limit, orderBy, isPublic, isRecruiting }: UseMyScrapsParams = {}) => { const query = useInfiniteQuery({ - queryKey: ["myScraps", params], - queryFn: async ({ pageParam = undefined }) => { + queryKey: ["myScraps", { limit, orderBy, isPublic, isRecruiting }], + queryFn: async ({ pageParam }) => { const response = await axios.get("/api/users/me/scrap", { params: { - ...params, cursor: pageParam, + limit, + orderBy, + isPublic: isPublic === undefined ? null : isPublic, + isRecruiting: isRecruiting === undefined ? null : isRecruiting, }, withCredentials: true, }); return response.data; }, - getNextPageParam: (lastPage) => lastPage.nextCursor, - initialPageParam: undefined, + initialPageParam: cursor, + getNextPageParam: (lastPage) => lastPage.nextCursor ?? null, staleTime: 1000 * 60 * 5, gcTime: 1000 * 60 * 30, }); return { ...query, - isPending: query.isPending, + data: query.data, + isLoading: query.isLoading, + isFetching: query.isFetching, error: query.error, }; }; diff --git a/src/hooks/queries/user/me/useUpdateProfile.ts b/src/hooks/queries/user/me/useUpdateProfile.ts new file mode 100644 index 00000000..e6bdb9aa --- /dev/null +++ b/src/hooks/queries/user/me/useUpdateProfile.ts @@ -0,0 +1,89 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import axios from "axios"; +import toast from "react-hot-toast"; + +interface UpdateProfileData { + name?: string; + nickname?: string; + phoneNumber?: string; + imageUrl?: string; + storeName?: string; + storePhoneNumber?: string; + location?: string; +} + +export const useUpdateProfile = () => { + const queryClient = useQueryClient(); + + 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 { + updateProfile, + isUpdating: updateProfileMutation.isPending || uploadImageMutation.isPending, + }; +}; diff --git a/src/hooks/queries/user/me/useUser.ts b/src/hooks/queries/user/me/useUser.ts index 1bcf4deb..0185c4bd 100644 --- a/src/hooks/queries/user/me/useUser.ts +++ b/src/hooks/queries/user/me/useUser.ts @@ -1,21 +1,9 @@ -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { useQuery } 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 () => { @@ -45,80 +33,11 @@ export const useUser = () => { 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/store/filterStore.ts b/src/store/filterStore.ts deleted file mode 100644 index 051f5cde..00000000 --- a/src/store/filterStore.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { create } from "zustand"; - -type FilterType = "isPublic" | "isRecruiting"; - -interface FilterState { - filterBy: { - isPublic: boolean; - isRecruiting: boolean; - }; - setFilterBy: (filterType: FilterType, value: string) => void; -} - -export const useFilterStore = create((set) => ({ - filterBy: { - isPublic: true, - isRecruiting: true, - }, - setFilterBy: (filterType, value) => - set((state) => ({ - filterBy: { - ...state.filterBy, - [filterType]: value, - }, - })), -})); diff --git a/src/utils/cleanedParameters.ts b/src/utils/cleanedParameters.ts new file mode 100644 index 00000000..7508e461 --- /dev/null +++ b/src/utils/cleanedParameters.ts @@ -0,0 +1,16 @@ +/** + * URL 쿼리 파라미터에서 null, undefined, 빈 문자열, 'null' 문자열을 제거하는 함수 + * @param params - 정제할 파라미터 객체 + * @returns 정제된 파라미터 객체 + */ +export const cleanedParameters = (params: Record) => { + return Object.entries(params).reduce( + (acc, [key, value]) => { + if (value !== null && value !== undefined && value !== "" && value !== "null") { + acc[key] = value; + } + return acc; + }, + {} as Record + ); +};