Skip to content

Commit

Permalink
신청자 리스트 불러오기 기능 구현 (#43)
Browse files Browse the repository at this point in the history
신청자 리스트 불러오기 기능 구현
  • Loading branch information
Dobbymin authored Nov 1, 2024
2 parents 623b4a0 + f882af8 commit 6e4fb1e
Show file tree
Hide file tree
Showing 14 changed files with 191 additions and 85 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ApplicantListResponse } from "./types";
import { fetchInstance } from "@/shared";

export const getApplicantListPath = () => `/admin/people`;

export const getApplicantList = async (page: number, per_page: number): Promise<ApplicantListResponse> => {
const response = await fetchInstance.get(getApplicantListPath(), {
params: {
page,
per_page,
},
});
return response.data;
};
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useGetApplicantList } from "./useGetApplicantList";
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getApplicantList, getApplicantListPath } from "../applicant-list.api";
import { ApplicantListResponse } from "../types";
import { useInfiniteQuery } from "@tanstack/react-query";

export const useGetApplicantList = (perPage: number) => {
return useInfiniteQuery<ApplicantListResponse, Error>({
queryKey: [getApplicantListPath(), perPage],
queryFn: ({ pageParam = 1 }) => getApplicantList(Number(pageParam), perPage),
initialPageParam: 1,
getNextPageParam: (lastPage) => {
const { currentPage, totalPages } = lastPage.pageInfo;
return currentPage < totalPages ? currentPage + 1 : undefined;
},
});
};
3 changes: 2 additions & 1 deletion packages/admin/src/features/people-management/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
// export * from "./hook";
export * from "./hook";
export * from "./types";
export * from "./mock";
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export type ApplyPeople = {
id: number;
studentId: number;
email: string;
githubId: string;
major: string;
name: string;
phoneNumber: string;
position: string;
teamName: string;
accepted: boolean;
checkined: boolean;
};

export type PageInfo = {
currentPage: number;
perPage: number;
totalItems: number;
totalPages: number;
};

export type ApplicantListResponse = {
status: number;
applyPeople: ApplyPeople[];
pageInfo: PageInfo;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type { ApplyPeople, PageInfo, ApplicantListResponse } from "./applicant-list.response";
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import { useState } from "react";
import { useState, useEffect } from "react";

import {
InputField,
PeopleTable,
useDeletePerson,
useUpdatePersonStatus,
useFilteredPeople,
useGetPeople,
useGetApplicantList,
ApplyPeople,
} from "@/features";

export const MainContents = () => {
const [searchTerm, setSearchTerm] = useState("");
const [people, setPeople] = useState<ApplyPeople[]>([]);
const { data, isLoading, fetchNextPage, hasNextPage } = useGetApplicantList(10);

const { data } = useGetPeople();

const { people, setPeople, deletePerson } = useDeletePerson(data);
useEffect(() => {
if (data) {
const fetchedPeople = data.pages.flatMap((page) => page.applyPeople);
setPeople(fetchedPeople);
}
}, [data]);

const updatePersonStatus = useUpdatePersonStatus(people, setPeople);

Expand All @@ -24,7 +29,13 @@ export const MainContents = () => {
<main className="flex-1 p-4 lg:p-8">
<h1 className="mb-6 text-2xl font-bold">인원 관리</h1>
<InputField searchTerm={searchTerm} setSearchTerm={setSearchTerm} />
<PeopleTable people={filteredPeople} updatePersonStatus={updatePersonStatus} deletePerson={deletePerson} />
<PeopleTable
people={filteredPeople}
updatePersonStatus={updatePersonStatus}
isLoading={isLoading}
hasNextPage={hasNextPage}
fetchNextPage={fetchNextPage}
/>
</main>
);
};
Original file line number Diff line number Diff line change
@@ -1,14 +1,41 @@
import { ChevronDown, X } from "lucide-react";
import { useState, useRef, useEffect, useCallback } from "react";

import { PeopleResponseData } from "../../api/mock/useGetPeople";
import { ChevronDown } from "lucide-react";

import { ApplyPeople } from "@/features";

type Props = {
people: PeopleResponseData[];
people?: ApplyPeople[];
isLoading: boolean;
hasNextPage: boolean;
fetchNextPage: () => void;
updatePersonStatus: (id: number, status: "accepted" | "rejected" | "checkedIn" | "checkedOut") => void;
deletePerson: (id: number) => void;
};

export const PeopleTable = ({ people, updatePersonStatus, deletePerson }: Props) => {
export const PeopleTable = ({ people, updatePersonStatus, isLoading, hasNextPage, fetchNextPage }: Props) => {
const [applyPeople, setApplyPeople] = useState<ApplyPeople[]>([]);
const observer = useRef<IntersectionObserver | null>(null);

useEffect(() => {
if (people) {
setApplyPeople(people);
}
}, [people]);

const lastPersonRef = useCallback(
(node: HTMLTableRowElement) => {
if (isLoading) return;
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasNextPage) {
fetchNextPage();
}
});
if (node) observer.current.observe(node);
},
[isLoading, hasNextPage, fetchNextPage],
);

return (
<div className="overflow-x-auto bg-white rounded-lg shadow">
<table className="w-full">
Expand All @@ -23,6 +50,9 @@ export const PeopleTable = ({ people, updatePersonStatus, deletePerson }: Props)
<th className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
전화번호
</th>
<th className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
팀 이름
</th>
<th className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
전공
</th>
Expand All @@ -32,57 +62,81 @@ export const PeopleTable = ({ people, updatePersonStatus, deletePerson }: Props)
<th className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
체크인/아웃
</th>
<th className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
삭제
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{people.map((person) => (
<tr key={person.id}>
<td className="px-6 py-4 whitespace-nowrap">{person.name}</td>
<td className="px-6 py-4 whitespace-nowrap">{person.studentId}</td>
<td className="px-6 py-4 whitespace-nowrap">{person.phoneNumber}</td>
<td className="px-6 py-4 whitespace-nowrap">{person.major}</td>
<td className="px-6 py-4 whitespace-nowrap">
<button
className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${
person.accepted
? "bg-green-100 text-green-800"
: "bg-yellow-100 text-yellow-800"
}`}
onClick={() =>
updatePersonStatus(person.id, person.accepted ? "rejected" : "accepted")
}
>
{person.accepted ? "승인됨" : "미승인"}
<ChevronDown className="w-4 h-4 ml-1" />
</button>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<button
className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${
person.checkedIn ? "bg-blue-100 text-blue-800" : "bg-gray-100 text-gray-800"
}`}
onClick={() =>
updatePersonStatus(person.id, person.checkedIn ? "checkedOut" : "checkedIn")
}
disabled={!person.accepted}
>
{person.checkedIn ? "체크인됨" : "미체크인"}
<ChevronDown className="w-4 h-4 ml-1" />
</button>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<button
className="text-red-600 hover:text-red-900"
onClick={() => deletePerson(person.id)}
>
<X className="w-5 h-5" />
</button>
</td>
</tr>
))}
{isLoading
? Array.from({ length: 10 }).map((_, idx) => (
<tr key={idx} className="animate-pulse">
<td className="px-6 py-4 whitespace-nowrap">
<div className="w-3/4 h-4 rounded bg-slate-200" />
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="w-1/2 h-4 rounded bg-slate-200" />
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="w-1/2 h-4 rounded bg-slate-200" />
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="w-1/2 h-4 rounded bg-slate-200" />
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="w-16 h-6 rounded bg-slate-200" />
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="w-16 h-6 rounded bg-slate-200" />
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="w-16 h-6 rounded bg-slate-200" />
</td>
</tr>
))
: applyPeople.map((person, index) => (
<tr
key={person.studentId}
ref={index === (people ? people.length - 1 : -1) ? lastPersonRef : null}
>
<td className="px-6 py-4 whitespace-nowrap">{person.name}</td>
<td className="px-6 py-4 whitespace-nowrap">{person.studentId}</td>
<td className="px-6 py-4 whitespace-nowrap">{person.phoneNumber}</td>
<td className="px-6 py-4 whitespace-nowrap">{person.teamName}</td>
<td className="px-6 py-4 whitespace-nowrap">{person.major}</td>
<td className="px-6 py-4 whitespace-nowrap">
<button
className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${
person.accepted
? "bg-green-100 text-green-800"
: "bg-yellow-100 text-yellow-800"
}`}
onClick={() =>
updatePersonStatus(person.id, person.accepted ? "rejected" : "accepted")
}
>
{person.accepted ? "승인됨" : "미승인"}
<ChevronDown className="w-4 h-4 ml-1" />
</button>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<button
className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${
person.checkined
? "bg-blue-100 text-blue-800"
: "bg-gray-100 text-gray-800"
}`}
onClick={() =>
updatePersonStatus(
person.id,
person.checkined ? "checkedOut" : "checkedIn",
)
}
disabled={!person.accepted}
>
{person.checkined ? "체크인됨" : "미체크인"}
<ChevronDown className="w-4 h-4 ml-1" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export { useDeletePerson } from "./useDeletePerson";
export { useFilteredPeople } from "./useFilteredPeople";
export { useUpdatePersonStatus } from "./useUpdatePersonStatus";

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { useMemo } from "react";

import { PeopleResponseData } from "../api/mock/useGetPeople";
import { ApplyPeople } from "../api";

export const useFilteredPeople = (people: PeopleResponseData[], searchTerm: string) => {
export const useFilteredPeople = (people: ApplyPeople[], searchTerm: string) => {
const filteredPeople = useMemo(() => {
return people.filter(
(person) =>
person.name.includes(searchTerm) ||
person.studentId.includes(searchTerm) ||
person.phoneNumber.includes(searchTerm) ||
person.studentId.toString().includes(searchTerm) ||
person.phoneNumber.toString().includes(searchTerm) ||
person.major.includes(searchTerm),
);
}, [people, searchTerm]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { PeopleResponseData } from "../api/mock/useGetPeople";
import { ApplyPeople } from "../api";

export const useUpdatePersonStatus = (
people: PeopleResponseData[],
setPeople: (people: PeopleResponseData[]) => void,
) => {
export const useUpdatePersonStatus = (people: ApplyPeople[], setPeople: (people: ApplyPeople[]) => void) => {
const updatePersonStatus = (id: number, status: "accepted" | "rejected" | "checkedIn" | "checkedOut") => {
setPeople(
people.map((person) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/admin/src/shared/api/instance/Instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const initInstance = (config: AxiosRequestConfig): AxiosInstance => {
};

export const fetchInstance = initInstance({
baseURL: "https://api.example.com",
baseURL: `https://k8q4ci7a8j.execute-api.us-east-1.amazonaws.com`,
});

export const queryClient = new QueryClient({
Expand Down

0 comments on commit 6e4fb1e

Please sign in to comment.