diff --git a/src/entities/projects/api/projectsAPi.ts b/src/entities/projects/api/projectsAPi.ts index b768db4..c52da18 100644 --- a/src/entities/projects/api/projectsAPi.ts +++ b/src/entities/projects/api/projectsAPi.ts @@ -1,23 +1,63 @@ -import { collection, getDocs } from "firebase/firestore/lite"; +import { + getCountFromServer, + collection, + getDocs, + limit, + orderBy, + query, + QueryDocumentSnapshot, + startAfter, + type DocumentData, +} from "firebase/firestore"; import type { ProjectListRes } from "@entities/projects/types/projects"; import { db } from "@shared/firebase/firebase"; +/** projects의 total 수 */ +export const getProjectsTotalCount = async (): Promise => { + try { + const q = query(collection(db, "projects")); + const querySnapshot = await getCountFromServer(q); + return querySnapshot.data().count; + } catch (err) { + console.log(err); + return 0; + } +}; + /** firebase project 목록 불러오기 */ -export const getProjectList = async (): Promise => { +export const getProjectList = async ({ + pageSize = 6, + lastDoc = null, +}: { + pageSize?: number; + lastDoc: QueryDocumentSnapshot | null; +}): Promise<{ + projects: ProjectListRes[]; + lastVisible: QueryDocumentSnapshot | null; +}> => { try { - const listRef = collection(db, "projects"); - const querySnapshot = await getDocs(listRef); + const baseQuery = query( + collection(db, "projects"), + orderBy("createdAt", "desc"), + limit(pageSize) + ); + const q = lastDoc ? query(baseQuery, startAfter(lastDoc)) : baseQuery; + + const querySnapshot = await getDocs(q); - const posts = querySnapshot.docs.map((doc) => ({ + const projects = querySnapshot.docs.map((doc) => ({ id: doc.id, ...doc.data(), - })); + })) as ProjectListRes[]; - return posts as ProjectListRes[]; + return { + projects: projects, + lastVisible: querySnapshot.docs[querySnapshot.docs.length - 1] || null, + }; } catch (err) { console.log(err); - return []; + return { projects: [], lastVisible: null }; } }; diff --git a/src/entities/projects/hook/useProjectPageNation.ts b/src/entities/projects/hook/useProjectPageNation.ts new file mode 100644 index 0000000..e76e29d --- /dev/null +++ b/src/entities/projects/hook/useProjectPageNation.ts @@ -0,0 +1,73 @@ +import { useState } from "react"; + +import useProjectList from "@entities/projects/queries/useProjectList"; +import type { LastVisibleType } from "@entities/projects/types/firebase"; +import type { ProjectListRes } from "@entities/projects/types/projects"; + +interface ReturnProjectPageNation { + projects: ProjectListRes[]; + currentPage: number; + paging: { + prev: () => void; + next: () => void; + reset: () => void; + disablePrev: boolean; + disableNext: boolean; + }; +} + +const useProjectPageNation = ({ + totalCount, + perPage = 6, +}: { + totalCount: number; + perPage?: number; +}): ReturnProjectPageNation => { + const [lastVisibleStack, setLastVisibleStack] = useState([ + null, + ]); + const [currentPage, setCurrentPage] = useState(0); + + const cursor = lastVisibleStack[currentPage] ?? null; + const { data: projects, isLoading } = useProjectList(cursor); + + const disablePrev = currentPage === 0 || isLoading; + const disableNext = + currentPage === Math.floor(totalCount / perPage) || isLoading; + + const pagingPrev = (): void => { + if (disablePrev) return; + + setCurrentPage((prev) => prev - 1); + }; + + const pagingNext = (): void => { + if (disableNext) return; + + if (projects?.lastVisible) { + if (currentPage === lastVisibleStack.length - 1) { + setLastVisibleStack((prev) => [...prev, projects.lastVisible]); + } + setCurrentPage((prev) => prev + 1); + } + }; + + const pagingReset = (): void => { + setLastVisibleStack([null]); + setCurrentPage(0); + }; + + return { + projects: projects?.projects || [], + currentPage, + paging: { + prev: pagingPrev, + next: pagingNext, + reset: pagingReset, + disablePrev, + disableNext, + }, + }; +}; + +export default useProjectPageNation; diff --git a/src/entities/projects/queries/useProjectList.ts b/src/entities/projects/queries/useProjectList.ts index c72600d..24b72b1 100644 --- a/src/entities/projects/queries/useProjectList.ts +++ b/src/entities/projects/queries/useProjectList.ts @@ -1,12 +1,18 @@ import { useQuery, type UseQueryResult } from "@tanstack/react-query"; +import type { DocumentData, QueryDocumentSnapshot } from "firebase/firestore"; import { getProjectList } from "@entities/projects/api/projectsAPi"; import type { ProjectListRes } from "@entities/projects/types/projects"; -const useProjectList = (): UseQueryResult => { +const useProjectList = ( + lastDoc: QueryDocumentSnapshot | null +): UseQueryResult<{ + projects: ProjectListRes[]; + lastVisible: QueryDocumentSnapshot | null; +}> => { return useQuery({ - queryKey: ["project-list"], - queryFn: getProjectList, + queryKey: ["project-list", lastDoc?.id ?? "none"], + queryFn: () => getProjectList({ pageSize: 6, lastDoc }), }); }; diff --git a/src/entities/projects/queries/useProjectsTotalCount.ts b/src/entities/projects/queries/useProjectsTotalCount.ts new file mode 100644 index 0000000..5e05808 --- /dev/null +++ b/src/entities/projects/queries/useProjectsTotalCount.ts @@ -0,0 +1,12 @@ +import { useQuery, type UseQueryResult } from "@tanstack/react-query"; + +import { getProjectsTotalCount } from "@entities/projects/api/projectsAPi"; + +const useProjectsTotalCount = (): UseQueryResult => { + return useQuery({ + queryKey: ["projects-total-count"], + queryFn: getProjectsTotalCount, + }); +}; + +export default useProjectsTotalCount; diff --git a/src/entities/projects/types/firebase.ts b/src/entities/projects/types/firebase.ts new file mode 100644 index 0000000..4ef85e9 --- /dev/null +++ b/src/entities/projects/types/firebase.ts @@ -0,0 +1,8 @@ +import type { + DocumentData, + QueryDocumentSnapshot, + Timestamp, +} from "firebase/firestore"; + +export type CreatedAt = Timestamp; +export type LastVisibleType = (QueryDocumentSnapshot | null)[]; diff --git a/src/entities/projects/types/projects.ts b/src/entities/projects/types/projects.ts index d7e2584..6633224 100644 --- a/src/entities/projects/types/projects.ts +++ b/src/entities/projects/types/projects.ts @@ -1,5 +1,4 @@ // 나중에 Project Owner 정보가 타입으로 들어가야할 것 같음 + expectedPeriod 타입 수정 or 포맷팅해서 DB 저장 - import type { UserRole } from "@shared/user/types/user"; // 팀원 목록들도 타입으로 들어가야할 것 같음, 이미지도 타입으로 들어가야할 것 같음 @@ -23,7 +22,6 @@ export interface ProjectItemInsertReq { positions: Positions[]; // 모집 포지션 applicants: string[]; // 지원자들 } - interface Positions { position: string; count: number; diff --git a/src/features/projects/api/projdectsApi.ts b/src/features/projects/api/projdectsApi.ts index 82ba6d3..166c671 100644 --- a/src/features/projects/api/projdectsApi.ts +++ b/src/features/projects/api/projdectsApi.ts @@ -1,4 +1,10 @@ -import { addDoc, collection, doc, setDoc } from "firebase/firestore/lite"; +import { + addDoc, + collection, + doc, + serverTimestamp, + setDoc, +} from "firebase/firestore/lite"; import type { ProjectItemInsertReq } from "@entities/projects/types/projects"; @@ -10,7 +16,10 @@ export const insertProjectItem = async ( ): Promise<{ success: boolean; message: string; id?: string }> => { try { const postsRef = collection(db, "projects"); - const docRef = await addDoc(postsRef, projectItem); + const docRef = await addDoc(postsRef, { + ...projectItem, + createdAt: serverTimestamp(), + }); return { success: true, diff --git a/src/pages/home/ui/HomePage.tsx b/src/pages/home/ui/HomePage.tsx index 05f07aa..2eeff00 100644 --- a/src/pages/home/ui/HomePage.tsx +++ b/src/pages/home/ui/HomePage.tsx @@ -3,14 +3,10 @@ import type { JSX } from "react"; import Hero from "@widgets/hero/ui/Hero"; -import useProjectList from "@entities/projects/queries/useProjectList"; import ProjectCard from "@entities/projects/ui/projects-card/ProjectCard"; import ProjectsStats from "@entities/projects/ui/projects-stats/ProjectsStats"; const HomePage = (): JSX.Element => { - const { data } = useProjectList(); - - console.log(data); console.log("API_KEY: ", import.meta.env.VITE_API_KEY); return ( diff --git a/src/pages/project-list/ui/ProjectList.tsx b/src/pages/project-list/ui/ProjectList.tsx new file mode 100644 index 0000000..eae4f92 --- /dev/null +++ b/src/pages/project-list/ui/ProjectList.tsx @@ -0,0 +1,33 @@ +import type { JSX } from "react"; + +import useProjectPageNation from "@entities/projects/hook/useProjectPageNation"; +import useProjectsTotalCount from "@entities/projects/queries/useProjectsTotalCount"; + +import PaginationBar from "@shared/ui/pagination"; + +const ProjectList = (): JSX.Element => { + const { data: totalCount = 0 } = useProjectsTotalCount(); + const { projects, currentPage, paging } = useProjectPageNation({ + totalCount, + }); + + return ( +
+ {projects.map((item, i) => { + return ( +
+ {item.id} - {item.title} +
+ ); + })} + + +
+ ); +}; + +export default ProjectList; diff --git a/src/pages/project-list/ui/ProjectListPage.tsx b/src/pages/project-list/ui/ProjectListPage.tsx index a1d0d8c..3aa633b 100644 --- a/src/pages/project-list/ui/ProjectListPage.tsx +++ b/src/pages/project-list/ui/ProjectListPage.tsx @@ -1,7 +1,14 @@ import type { JSX } from "react"; +import ProjectList from "@pages/project-list/ui/ProjectList"; + const ProjectListPage = (): JSX.Element => { - return
ProjectListPage
; + return ( +
+ ProjectListPage + +
+ ); }; export default ProjectListPage; diff --git a/src/shared/firebase/firebase.ts b/src/shared/firebase/firebase.ts index 7c0896c..ff8bf0f 100644 --- a/src/shared/firebase/firebase.ts +++ b/src/shared/firebase/firebase.ts @@ -1,6 +1,6 @@ import { initializeApp } from "firebase/app"; import { getAuth, GoogleAuthProvider, GithubAuthProvider } from "firebase/auth"; -import { getFirestore } from "firebase/firestore/lite"; +import { getFirestore } from "firebase/firestore"; const firebaseConfig = { apiKey: import.meta.env.VITE_API_KEY, diff --git a/src/shared/libs/utils/paginnation.ts b/src/shared/libs/utils/paginnation.ts new file mode 100644 index 0000000..60c4aff --- /dev/null +++ b/src/shared/libs/utils/paginnation.ts @@ -0,0 +1,11 @@ +export const makePagingArr = (totalCount: number): number[] => { + const perPage = 6; + const lastPageNum = + totalCount % perPage > 0 + ? Math.floor(totalCount / perPage) + 1 + : Math.floor(totalCount / perPage); + + const pageArray = Array.from({ length: lastPageNum }, (_, i) => i + 1); + + return pageArray; +}; diff --git a/src/shared/ui/pagination.tsx b/src/shared/ui/pagination.tsx new file mode 100644 index 0000000..24cb8e8 --- /dev/null +++ b/src/shared/ui/pagination.tsx @@ -0,0 +1,47 @@ +import { Box } from "@mui/material"; +import type { JSX } from "react"; + +import { makePagingArr } from "@shared/libs/utils/paginnation"; + +interface PaginationProps { + totalCount?: number; + currentPage: number; + prev: () => void; + next: () => void; + reset: () => void; + disablePrev: boolean; + disableNext: boolean; +} + +const PaginationBar = ({ + totalCount = 0, + currentPage, + prev, + next, + reset, + disablePrev, + disableNext, +}: PaginationProps): JSX.Element => { + const paginArray = makePagingArr(totalCount); + + return ( + + + + {paginArray.map((num, i) => ( + + {num} + + ))} + + + + + ); +}; + +export default PaginationBar;