diff --git a/src/app/api/resume/route.ts b/src/app/api/resume/route.ts index 8cb83981b..d74b4a35b 100644 --- a/src/app/api/resume/route.ts +++ b/src/app/api/resume/route.ts @@ -3,31 +3,62 @@ import { getServerSession } from 'next-auth'; import { authOptions } from '@/utils/auth-option'; import { prisma } from '@/lib/prisma'; import { AUTH_MESSAGE, RESUME_MESSAGE } from '@/constants/message-constants'; -import { SEARCH_PARAMS } from '@/constants/resume-constants'; import { ENV } from '@/constants/env-constants'; import { getToken } from 'next-auth/jwt'; +import { sanitizeQueryParams } from '@/utils/sanitize-query-params'; const { NEXTAUTH_SECRET } = ENV; const { EXPIRED_TOKEN } = AUTH_MESSAGE.ERROR; const { AUTH_REQUIRED } = AUTH_MESSAGE.RESULT; -const { NOT_FOUND, GET_SERVER_ERROR } = RESUME_MESSAGE; -const { STATUS } = SEARCH_PARAMS; +const { GET_SERVER_ERROR } = RESUME_MESSAGE; export const GET = async (request: NextRequest) => { try { const token = await getToken({ req: request, secret: NEXTAUTH_SECRET }); if (!token) return NextResponse.json({ message: EXPIRED_TOKEN }, { status: 401 }); - const session = await getServerSession(authOptions); if (!session || !session.user) { return NextResponse.json({ message: AUTH_REQUIRED }, { status: 401 }); } + const userId = session.user.id; + const searchParams = request.nextUrl.searchParams; + + const { + page = undefined, + limit = undefined, + status = undefined, + reqType = undefined, + } = sanitizeQueryParams(searchParams); - const status = request.nextUrl.searchParams.get(STATUS); + if (reqType === 'infinity') { + const pageNumber = Number(page); + const limitNumber = Number(limit); + + if (isNaN(pageNumber) || isNaN(limitNumber)) { + return NextResponse.json({ message: '유효하지 않은 파라미터입니다.' }, { status: 400 }); + } + const response = await prisma.resume.findMany({ + where: { + userId: userId, + status: Number(status), + }, + orderBy: { + createdAt: 'desc', + }, + skip: (pageNumber - 1) * limitNumber, + take: limitNumber, + }); + + const totalCount = await prisma.resume.count({ + where: { userId, status: Number(status) }, + }); + const nextPage = pageNumber * limitNumber < totalCount ? pageNumber + 1 : null; + return NextResponse.json({ response, nextPage }, { status: 200 }); + } const response = await prisma.resume.findMany({ where: { - userId: session.user.id, + userId: userId, status: Number(status), }, orderBy: { @@ -35,10 +66,6 @@ export const GET = async (request: NextRequest) => { }, }); - if (!response) { - return NextResponse.json({ message: NOT_FOUND }, { status: 404 }); - } - return NextResponse.json({ response }, { status: 200 }); } catch (error) { return NextResponse.json({ message: GET_SERVER_ERROR }, { status: 500 }); diff --git a/src/constants/resume-constants.ts b/src/constants/resume-constants.ts index fdbcffbbf..f1ebe6717 100644 --- a/src/constants/resume-constants.ts +++ b/src/constants/resume-constants.ts @@ -1,7 +1,7 @@ export const RESUME_STATUS = { DRAFT: 0, SUBMIT: 1, -}; +} as const; export const AUTO_SAVE_STATUS = { SAVING: '자동 저장 중', diff --git a/src/features/interview/resume-all-modal.tsx b/src/features/interview/resume-all-modal.tsx index c8104aced..c8d195f15 100644 --- a/src/features/interview/resume-all-modal.tsx +++ b/src/features/interview/resume-all-modal.tsx @@ -58,7 +58,6 @@ const ResumeAllModal = () => { setResume(resume.id); toggleModal(ALL_RESUME_LIST); }} - hrOption={false} /> ); })} diff --git a/src/features/resume-list/hooks/use-resume-infinite-query.ts b/src/features/resume-list/hooks/use-resume-infinite-query.ts new file mode 100644 index 000000000..74363a87b --- /dev/null +++ b/src/features/resume-list/hooks/use-resume-infinite-query.ts @@ -0,0 +1,27 @@ +import { QUERY_KEY } from '@/constants/query-key'; +import { RESUME_STATUS } from '@/constants/resume-constants'; +import { useInfiniteQuery } from '@tanstack/react-query'; +import { getResumeListByInfinite } from '@/features/resume/api/client-services'; + +const { RESUMES } = QUERY_KEY; + +const ITEM_PER_PAGE = 8; +export const useResumeInfiniteQuery = (status: (typeof RESUME_STATUS)[keyof typeof RESUME_STATUS]) => { + return useInfiniteQuery({ + queryKey: [RESUMES, 'infinity'], + queryFn: async ({ pageParam = 1 }) => { + const params = { + status, + pageParam, + limit: ITEM_PER_PAGE, + reqType: 'infinity', + }; + + const response = await getResumeListByInfinite(params); + return response; + }, + getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined, + initialPageParam: 1, + refetchIntervalInBackground: false, + }); +}; diff --git a/src/features/resume-list/resume-item.tsx b/src/features/resume-list/resume-item.tsx index a8fcdc13d..e8868cc34 100644 --- a/src/features/resume-list/resume-item.tsx +++ b/src/features/resume-list/resume-item.tsx @@ -6,26 +6,26 @@ import clsx from 'clsx'; type Props = { resume: ResumeType; onClick: (resumeId: ResumeType['id']) => void; - hrOption?: boolean; + isLastChild?: boolean; }; -const ResumeItem = ({ resume, onClick, hrOption = true }: Props) => { +const ResumeItem = ({ resume, onClick, isLastChild = false }: Props) => { const { id, title, createdAt, tryCount } = resume; const hasNotInterviewed = tryCount === 0; return ( -