Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
47 changes: 37 additions & 10 deletions src/app/api/resume/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,69 @@ 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: {
createdAt: 'desc',
},
});

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 });
Expand Down
2 changes: 1 addition & 1 deletion src/constants/resume-constants.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export const RESUME_STATUS = {
DRAFT: 0,
SUBMIT: 1,
};
} as const;

export const AUTO_SAVE_STATUS = {
SAVING: '자동 저장 중',
Expand Down
1 change: 0 additions & 1 deletion src/features/interview/resume-all-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ const ResumeAllModal = () => {
setResume(resume.id);
toggleModal(ALL_RESUME_LIST);
}}
hrOption={false}
/>
);
})}
Expand Down
27 changes: 27 additions & 0 deletions src/features/resume-list/hooks/use-resume-infinite-query.ts
Original file line number Diff line number Diff line change
@@ -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,
});
};
10 changes: 5 additions & 5 deletions src/features/resume-list/resume-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<li onClick={() => onClick(id)} className={clsx(hrOption && 'border-b', 'cursor-pointer py-2')}>
<li onClick={() => onClick(id)} className={clsx('cursor-pointer py-2', isLastChild ? 'border-b-0' : 'border-b')}>
<Typography size='sm' weight='normal' color='gray-500'>
{formatDate({ input: createdAt })}
</Typography>
<div className='flex items-end justify-between'>
<Typography weight='bold'>{title}</Typography>
{hasNotInterviewed ? (
<Typography size='sm' as='span' weight='bold' color='gray-500'>
<Typography size='sm' weight='bold' color='gray-500' as='span'>
면접 보기 전
</Typography>
) : (
<Typography size='sm' weight='bold' as='span' color='primary-600'>
<Typography size='sm' weight='bold' color='primary-600' as='span'>
{tryCount}회 면접 완료
</Typography>
)}
Expand Down
30 changes: 20 additions & 10 deletions src/features/resume-list/resume-list.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
'use client';
import { useRouter } from 'next/navigation';
import LoadingAnimation from '@/components/common/loading-animation';
import ResumeItem from '@/features/resume-list/resume-item';
import { useResumeListQuery } from '@/features/resume-list/hooks/use-resume-list-query';
import { getMyPagePath } from '@/features/my-page/utils/get-my-page-path';
import { TABS } from '@/constants/my-page-constants';
import { RESUME_STATUS } from '@/constants/resume-constants';
import { getMyPagePath } from '@/features/my-page/utils/get-my-page-path';
import { useResumeInfiniteQuery } from '@/features/resume-list/hooks/use-resume-infinite-query';
import ResumeItem from '@/features/resume-list/resume-item';
import { useInfiniteScroll } from '@/hooks/customs/use-infinite-scroll';
import { useRouter } from 'next/navigation';

const { RESUME_TAB } = TABS;

const { SUBMIT } = RESUME_STATUS;
const ResumeList = () => {
const router = useRouter();

const { data: resumeList, isPending, isError } = useResumeListQuery();
const { data, isPending, isError, fetchNextPage, hasNextPage, isFetchingNextPage } = useResumeInfiniteQuery(SUBMIT);
const targetRef = useInfiniteScroll({
fetchNextPage,
hasNextPage,
});

const handleGetDetailList = (resumeId: number) => {
router.push(getMyPagePath(RESUME_TAB, resumeId));
Expand All @@ -27,18 +32,23 @@ const ResumeList = () => {

if (isError) return <div>자소서 리스트를 불러오는데 실패하였습니다.</div>;

const resumes = data.pages.flatMap((page) => page.response);

return (
<ul className='h-full overflow-y-auto scrollbar-hide'>
{resumeList.map((resume, index) => {
{resumes?.map((resume, index) => {
return (
<ResumeItem
key={resume.id}
key={`resume_list_${resume.id}_${index}`}
resume={resume}
hrOption={resumeList.length !== index + 1}
onClick={handleGetDetailList}
isLastChild={resumes.length === index + 1}
/>
);
})}
<div className='flex h-10 w-full items-center justify-center text-sm text-gray-400' ref={targetRef}>
{isFetchingNextPage && <span>로딩 중..</span>}
</div>
</ul>
);
};
Expand Down
45 changes: 20 additions & 25 deletions src/features/resume/api/client-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { API_HEADER, API_METHOD } from '@/constants/api-method-constants';
import { ROUTE_HANDLER_PATH } from '@/constants/path-constant';
import type { ResumeData } from '@/types/resume';
import { fetchWithSentry } from '@/utils/fetch-with-sentry';
import type { Resume } from '@prisma/client';
import { ResumeType } from '@/types/DTO/resume-dto';

type Props = {
data: ResumeData;
Expand All @@ -14,7 +14,7 @@ const { GET, POST, PATCH, DELETE } = API_METHOD;
const { JSON_HEADER } = API_HEADER;

type SubmitParams = {
resumeId: Resume['id'];
resumeId: ResumeType['id'];
type: typeof POST | typeof PATCH;
};
/**
Expand Down Expand Up @@ -64,7 +64,7 @@ export const autoSaveResume = async ({ resumeId, data }: Props) => {
* @param {Number} status 저장 상태(등록/임시 저장)
* @returns resumes 상태에 따른 자소서 리스트
*/
export const getResumeList = async (status: number): Promise<Resume[]> => {
export const getResumeList = async (status: number): Promise<ResumeType[]> => {
const url = `${ROOT}?status=${status}`;

const { response: resumeList } = await fetchWithSentry(url, {
Expand All @@ -78,42 +78,37 @@ type ResumeListProps = {
status: number;
pageParam: number;
limit: number;
reqType: string;
};

type PaginatedResumeListResponse = {
resumeList: Resume[];
nextPage: number | null;
};
/**
*
* @param {Number} status 저장 상태(등록/임시 저장)
* @param {Number} pageParam 가져올 페이지 번호
* @param {Number} limit 한 페이지에 표시할 항목 수
* @returns resumeList 자소서 목록
* @returns nextPage 다음 페이지
* 특정 개수 단위로 자소서 data를 서버에서 받아옴
* @param params - { status : 받아올 자소서의 상태 , pageParams : 초기 page, limit : 받아올 데이터의 개수 }
* @returns page,다음 page, 해당 page에 들어있는 data
*/
export const getPaginatedResumeList = async ({
status,
pageParam,
limit,
}: ResumeListProps): Promise<PaginatedResumeListResponse> => {
const url = `${ROOT}?status=${status}&page=${pageParam}&limit=${limit}`;
export const getResumeListByInfinite = async (params: ResumeListProps) => {
const { pageParam, limit, status, reqType } = params;
const queryParams = new URLSearchParams({
page: pageParam.toString(),
limit: limit.toString(),
status: status.toString(),
reqType,
});

const { response: resumeList } = await fetchWithSentry(url, {
const url = `${ROOT}?${queryParams}`;

const data: { response: ResumeType[]; nextPage: number | null } = await fetchWithSentry(url, {
method: GET,
});

const hasNextPage = resumeList.length === limit;
const nextPage = hasNextPage ? pageParam + 1 : null;

return { resumeList, nextPage };
return data;
};

/**
* DB에서 원하는 자소서를 불러오는 요청
* @returns {Array} draftResumes 임시 저장된 자소서 리스트
*/
export const getResume = async (resumeId: number): Promise<Resume> => {
export const getResume = async (resumeId: number): Promise<ResumeType> => {
const { response: resume } = await fetchWithSentry(DETAIL(resumeId), {
method: GET,
});
Expand Down