Skip to content

Commit

Permalink
Feature/#150 학생의 마일스톤 획득내역 실적목록 페이지네이션 처리 (#151)
Browse files Browse the repository at this point in the history
* feat: 클라이언트 서비스의 페이지네이션 컴포넌트 구현

#141

* feat: 학생의 마일스톤 획득내역, 실적 목록 페이지네이션 처리

* feat: 나의 마일스톤 획득 내역에 페이지네이션 처리

#150

* fix: 학생의 실적 목록 조회 api의 totalElements가 제대로 불러와지지 않는 문제 해결

#150

---------

Co-authored-by: amaran-th <[email protected]>
  • Loading branch information
amaran-th and amaran-th authored Aug 22, 2024
1 parent 237a6e2 commit 45bfea1
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ public static Page<MilestoneHistoryOfStudentResponse> from(final Page<MilestoneH
milestoneHistory.getActivatedAt(),
milestoneHistory.getCreatedAt()
))
.toList());
.toList(), milestoneHistories.getPageable(), milestoneHistories.getTotalElements());
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
/* eslint-disable jsx-a11y/control-has-associated-label */
/* eslint-disable max-len */
import Pagination from '@/app/components/Pagination';
import MilestoneGroupLabel from '@/components/MilestoneGroupLabel';
import { MilestoneHistoryStatus } from '@/data/milestone';
import { useAppSelector } from '@/lib/hooks/redux';
import { useMilestoneHistoriesOfStudentQuery } from '@/lib/hooks/useApi';
import { Period } from '@/types/common';
import { MilestoneHistorySortCriteria, SortDirection } from '@/types/milestone';
import { usePathname } from 'next/navigation';

interface MilestoneHistoryTableProps {
searchFilterPeriod: Period;
Expand All @@ -14,6 +16,7 @@ interface MilestoneHistoryTableProps {
}

const MilestoneHistoryTable = ({ searchFilterPeriod, pageNumber, pageSize }: MilestoneHistoryTableProps) => {
const pathname = usePathname();
const auth = useAppSelector((state) => state.auth).value;
const { data: milestoneHistoriesOfStudent } = useMilestoneHistoriesOfStudentQuery(
auth.uid,
Expand All @@ -22,36 +25,44 @@ const MilestoneHistoryTable = ({ searchFilterPeriod, pageNumber, pageSize }: Mil
MilestoneHistoryStatus.APPROVED,
MilestoneHistorySortCriteria.ACTIVATED_AT,
SortDirection.DESC,
pageNumber,
pageNumber - 1,
pageSize,
);
return (
<table className="w-full border-collapse">
<thead>
<tr className="flex items-center border-b border-border text-center text-sm sm:text-base">
<th className="flex-grow p-[10px]">활동명</th>
<th className="w-16 p-1 sm:w-20 sm:p-[10px]">구분</th>
<th className="w-16 p-1 sm:p-[10px]">점수</th>
<th className="hidden w-[80px] p-1 sm:table-cell sm:w-[100px] sm:p-[10px]">활동일</th>
</tr>
</thead>
<tbody className="border-y-2 border-border text-xs sm:text-sm">
{milestoneHistoriesOfStudent?.content.map((milestoneHistory) => (
<tr key={milestoneHistory.id} className="flex items-center border-b border-border text-center">
<td className="max-w-[calc(100%-128px)] flex-grow p-[10px] text-left sm:max-w-[calc(100%-244px)]">
{milestoneHistory.description}
</td>
<td className="w-16 p-1 sm:w-20 sm:p-[10px]">
<MilestoneGroupLabel group={milestoneHistory.milestone.categoryGroup} />
</td>
<td className="w-16 p-1 sm:p-[10px]">{milestoneHistory.milestone.score * milestoneHistory.count}</td>
<td className="hidden w-[100px] p-[10px] sm:table-cell">
{milestoneHistory.activatedAt.slice(0, 10).replaceAll('-', '.')}
</td>
<div className="flex flex-col gap-4">
<table className="w-full border-collapse">
<thead>
<tr className="flex items-center border-b border-border text-center text-sm sm:text-base">
<th className="flex-grow p-[10px]">활동명</th>
<th className="w-16 p-1 sm:w-20 sm:p-[10px]">구분</th>
<th className="w-16 p-1 sm:p-[10px]">점수</th>
<th className="hidden w-[80px] p-1 sm:table-cell sm:w-[100px] sm:p-[10px]">활동일</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody className="border-y-2 border-border text-xs sm:text-sm">
{milestoneHistoriesOfStudent?.content.map((milestoneHistory) => (
<tr key={milestoneHistory.id} className="flex items-center border-b border-border text-center">
<td className="max-w-[calc(100%-128px)] flex-grow p-[10px] text-left sm:max-w-[calc(100%-244px)]">
{milestoneHistory.description}
</td>
<td className="w-16 p-1 sm:w-20 sm:p-[10px]">
<MilestoneGroupLabel group={milestoneHistory.milestone.categoryGroup} />
</td>
<td className="w-16 p-1 sm:p-[10px]">{milestoneHistory.milestone.score * milestoneHistory.count}</td>
<td className="hidden w-[100px] p-[10px] sm:table-cell">
{milestoneHistory.activatedAt.slice(0, 10).replaceAll('-', '.')}
</td>
</tr>
))}
</tbody>
</table>
<Pagination
currentPage={pageNumber}
pageSize={pageSize}
totalItems={milestoneHistoriesOfStudent?.totalElements ?? 0}
pathname={pathname}
/>
</div>
);
};

Expand Down
15 changes: 12 additions & 3 deletions frontend/src/app/(withSidebar)/my-page/milestone/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { DateTime } from 'luxon';
import { useState } from 'react';
import { useEffect, useState } from 'react';

import { COLOR } from '@/constants';
import { Period } from '@/types/common';
Expand All @@ -10,14 +10,24 @@ import MilestoneHistoryTable from './components/MilestoneHistoryTable';
import MilestoneOverview from './components/MilestoneOverview';
import { Content, SubTitle } from './styled';
import MilestonePeriodSearchForm from '../../../../components/MilestonePeriodSearchForm';
import { useSearchParams } from 'next/navigation';

const Page = () => {
const searchParams = useSearchParams();
const [pageNumber, setPageNumber] = useState<number>(1);
const [filterPeriod, setFilterPeriod] = useState<Period>({
startDate: DateTime.now().minus({ years: 1 }).toFormat('yyyy-MM-dd'),
endDate: DateTime.now().toFormat('yyyy-MM-dd'),
});
const [searchFilterPeriod, setSearchFilterPeriod] = useState<Period>(filterPeriod);

useEffect(() => {
const pageParam = searchParams.get('page');
if (pageParam) {
setPageNumber(parseInt(pageParam, 10));
}
}, [searchParams]);

return (
<Content>
<div className="mb-6 flex flex-wrap justify-between gap-4">
Expand All @@ -32,8 +42,7 @@ const Page = () => {
<MilestoneOverview searchFilterPeriod={searchFilterPeriod} />
<div style={{ borderBottom: `1px dotted ${COLOR.border}`, margin: '30px 0px' }} />
<SubTitle>획득 내역</SubTitle>
{/* TODO 제대로 페이지네이션 처리 하기 */}
<MilestoneHistoryTable searchFilterPeriod={searchFilterPeriod} pageNumber={0} pageSize={10} />
<MilestoneHistoryTable searchFilterPeriod={searchFilterPeriod} pageNumber={pageNumber} pageSize={5} />
</Content>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,17 @@ import { getAuthFromCookie } from '@/lib/utils/auth';
import { MilestoneHistorySortCriteria, SortDirection } from '@/types/milestone';

import MilestoneHistoryDeleteButton from '../MilestoneHistoryDeleteButton';
import Pagination from '@/app/components/Pagination';
import { headers } from 'next/headers';

interface MilestoneHistoryTableProp {
pageNumber: number;
}

const MilestoneHistoryTable = async ({ pageNumber }: MilestoneHistoryTableProp) => {
const headersList = headers();
const pathname = headersList.get('x-pathname') || '';

const MilestoneHistoryTable = async () => {
const auth = getAuthFromCookie();
const milestoneHistories = await getMilestoneHistoriesOfStudent(
auth.uid,
Expand All @@ -17,57 +26,68 @@ const MilestoneHistoryTable = async () => {
undefined,
MilestoneHistorySortCriteria.CREATED_AT,
SortDirection.DESC,
pageNumber - 1,
);

return (
<table className="w-full text-sm">
<thead className="font-bold">
<tr className="text-center">
<td className="min-w-[2em] p-2">No</td>
<td className="hidden w-full p-2 sm:table-cell">제목</td>
<td className="min-w-[3em] p-2">점수</td>
<td className="hidden min-w-[8em] p-2 sm:table-cell">활동일</td>
<td className="hidden min-w-[8em] p-2 sm:table-cell">등록일</td>
<td className="hidden min-w-[6em] p-2 sm:table-cell">진행 상황</td>
<td className="hidden min-w-[6em] p-2 sm:table-cell">처리</td>
<td className="table-cell min-w-[6em] p-2 sm:hidden">실적 내역</td>
</tr>
</thead>
<tbody className="border-y text-center">
{milestoneHistories?.content.map((milestoneHistory, index) => (
<tr className="border-b border-border p-2">
<td className="p-2">{index + 1} </td>
<td className="hidden p-2 text-left sm:table-cell">{milestoneHistory.description}</td>
<td className="p-2">{milestoneHistory.milestone.score * milestoneHistory.count}</td>
<td className="hidden p-2 sm:table-cell">{milestoneHistory.activatedAt.replaceAll('-', '.')}</td>
<td className="hidden p-2 sm:table-cell">{milestoneHistory.createdAt.slice(0, 10).replaceAll('-', '.')}</td>
<td className="hidden p-2 sm:table-cell" align="center">
<MilestoneHistoryStatusLabel
status={milestoneHistory.status}
rejectReason={milestoneHistory.rejectReason}
/>
</td>
<td className="hidden p-2 sm:table-cell">
<MilestoneHistoryDeleteButton historyId={milestoneHistory.id} />
</td>
<td className="flex flex-col gap-1 p-2 sm:hidden">
<div className="flex items-center justify-start gap-1">
<div className="flex flex-col gap-4">
<table className="w-full text-sm">
<thead className="font-bold">
<tr className="text-center">
<td className="min-w-[2em] p-2">No</td>
<td className="hidden w-full p-2 sm:table-cell">제목</td>
<td className="min-w-[3em] p-2">점수</td>
<td className="hidden min-w-[8em] p-2 sm:table-cell">활동일</td>
<td className="hidden min-w-[8em] p-2 sm:table-cell">등록일</td>
<td className="hidden min-w-[6em] p-2 sm:table-cell">진행 상황</td>
<td className="hidden min-w-[6em] p-2 sm:table-cell">처리</td>
<td className="table-cell min-w-[6em] p-2 sm:hidden">실적 내역</td>
</tr>
</thead>
<tbody className="border-y text-center">
{milestoneHistories?.content.map((milestoneHistory, index) => (
<tr className="border-b border-border p-2">
<td className="p-2">{index + 1} </td>
<td className="hidden p-2 text-left sm:table-cell">{milestoneHistory.description}</td>
<td className="p-2">{milestoneHistory.milestone.score * milestoneHistory.count}</td>
<td className="hidden p-2 sm:table-cell">{milestoneHistory.activatedAt.replaceAll('-', '.')}</td>
<td className="hidden p-2 sm:table-cell">
{milestoneHistory.createdAt.slice(0, 10).replaceAll('-', '.')}
</td>
<td className="hidden p-2 sm:table-cell" align="center">
<MilestoneHistoryStatusLabel
status={milestoneHistory.status}
rejectReason={milestoneHistory.rejectReason}
/>
<div className="flex-grow text-left font-bold">{milestoneHistory.description}</div>
</div>
<div className="flex justify-between text-xs text-comment">
<div>활동: {milestoneHistory.activatedAt.replaceAll('-', '.')}</div>
<div>등록: {milestoneHistory.createdAt.slice(0, 10).replaceAll('-', '.')}</div>
</div>
<MilestoneHistoryDeleteButton historyId={milestoneHistory.id} />
</td>
</tr>
))}
</tbody>
</table>
</td>
<td className="hidden p-2 sm:table-cell">
<MilestoneHistoryDeleteButton historyId={milestoneHistory.id} />
</td>
<td className="flex flex-col gap-1 p-2 sm:hidden">
<div className="flex items-center justify-start gap-1">
<MilestoneHistoryStatusLabel
status={milestoneHistory.status}
rejectReason={milestoneHistory.rejectReason}
/>
<div className="flex-grow text-left font-bold">{milestoneHistory.description}</div>
</div>
<div className="flex justify-between text-xs text-comment">
<div>활동: {milestoneHistory.activatedAt.replaceAll('-', '.')}</div>
<div>등록: {milestoneHistory.createdAt.slice(0, 10).replaceAll('-', '.')}</div>
</div>
<MilestoneHistoryDeleteButton historyId={milestoneHistory.id} />
</td>
</tr>
))}
</tbody>
</table>
<Pagination
currentPage={pageNumber}
pageSize={10}
totalItems={milestoneHistories?.totalElements ?? 0}
pathname={pathname}
/>
</div>
);
};

Expand Down
23 changes: 13 additions & 10 deletions frontend/src/app/(withSidebar)/my-page/milestone/register/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@ import PageTitle from '@/app/components/PageTitle';

import MilestoneHistoryTable from './components/MilestoneHistoryTable';

const Page = async () => (
<div className="rounded-sm bg-white p-5">
<div className="mb-10 flex items-center justify-between">
<PageTitle title="실적 등록" description="나의 마일스톤 실적 결과 등록" urlText="" url="" />
<Link href="/my-page/milestone/register/write" className="rounded-sm bg-primary-main px-5 py-1 text-white">
실적 등록
</Link>
const Page = async ({ searchParams }: { searchParams?: { [key: string]: string | undefined } }) => {
const pageNumber = searchParams?.page ? parseInt(searchParams.page, 10) : 1;
return (
<div className="rounded-sm bg-white p-5">
<div className="mb-10 flex items-center justify-between">
<PageTitle title="실적 등록" description="나의 마일스톤 실적 결과 등록" urlText="" url="" />
<Link href="/my-page/milestone/register/write" className="rounded-sm bg-primary-main px-5 py-1 text-white">
실적 등록
</Link>
</div>
<MilestoneHistoryTable pageNumber={pageNumber} />
</div>
<MilestoneHistoryTable />
</div>
);
);
};

export default Page;
52 changes: 52 additions & 0 deletions frontend/src/app/components/Pagination/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* eslint-disable prettier/prettier */
/* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable max-len */
import { VscChevronLeft } from '@react-icons/all-files/vsc/VscChevronLeft';
import { VscChevronRight } from '@react-icons/all-files/vsc/VscChevronRight';
import Link from 'next/link';

export interface PaginationProps {
currentPage: number;
pageSize: number;
totalItems: number;
pathname: string;
query?: string;
}

const Pagination = ({ currentPage, pageSize, totalItems, pathname, query }: PaginationProps) => {
const buttonPerPage = 10;

const totalPageCount = Math.ceil(totalItems / pageSize);
const totalPages = Array.from({ length: totalPageCount }, (v, i) => i + 1);

const showIdx = Math.floor((currentPage - 1) / buttonPerPage);
const showPage = totalPages.slice(showIdx * buttonPerPage, (showIdx + 1) * buttonPerPage);

const queries = query ? JSON.parse(query) : null;

const nextPageCalc = (showIdx + 1) * buttonPerPage + 1;
const prevPage = showIdx === 0 ? 1 : showIdx * buttonPerPage;
const nextPage = totalPageCount >= nextPageCalc ? nextPageCalc : totalPageCount;

return (
<div className="flex w-full justify-center gap-4 [&>*]:rounded-sm [&>*]:border-[1px] [&>*]:border-admin-border">
<Link href={{ pathname, query: { ...queries, page: prevPage } }} className="h-8 w-8 p-1">
<VscChevronLeft className="h-full w-full text-base" />
</Link>
{showPage.map((page) => (
<Link
href={{ pathname, query: { ...queries, page } }}
key={page}
className={`flex h-8 w-8 items-center justify-center p-1 text-sm ${currentPage === page && 'bg-primary-main text-white'}`}
>
{page}
</Link>
))}
<Link href={{ pathname, query: { ...queries, page: nextPage } }} className="h-8 w-8 p-1">
<VscChevronRight className="h-full w-full text-base" />
</Link>
</div>
);
};

export default Pagination;

0 comments on commit 45bfea1

Please sign in to comment.