Skip to content

Commit 07bc456

Browse files
committed
refactor: 어드민 내부 목록 관련 개선
1 parent 31e1bbc commit 07bc456

17 files changed

Lines changed: 156 additions & 180 deletions

File tree

apps/pyconkr-admin/src/components/elements/inline_resource_section.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { BackendAPIClientError } from "@frontend/common/apis/client";
22
import {
33
useBackendAdminClient,
44
useCreateMutation,
5-
useListAutoQuery,
5+
useListPaginatedQuery,
66
useRemovePreparedMutation,
77
useUpdatePreparedMutation,
88
} from "@frontend/common/hooks/useAdminAPI";
@@ -280,17 +280,17 @@ export const InlineResourceSection: FC<InlineResourceSectionProps> = ErrorBounda
280280
{ fallback: ErrorFallback },
281281
Suspense.with({ fallback: <CircularProgress /> }, ({ app, resource, filter, label, columns, hideCreatedAt, orderField, dialogChildren }) => {
282282
const client = useBackendAdminClient();
283-
const listQuery = useListAutoQuery<ResourceRow>(client, app, resource, { [filter.key]: filter.value, page_size: "100" });
283+
const listQuery = useListPaginatedQuery<ResourceRow>(client, app, resource, { [filter.key]: filter.value, page_size: "100" });
284284
const removeMutation = useRemovePreparedMutation(client, app, resource);
285285
const updateMutation = useUpdatePreparedMutation<ResourceRow & { id: string }>(client, app, resource);
286286
const [dialog, setDialog] = useState<{ open: boolean; item?: ResourceRow }>({ open: false });
287287
const [reordering, setReordering] = useState(false);
288288

289289
const items = useMemo(() => {
290-
const raw = listQuery.data.items ?? [];
290+
const raw = listQuery.data.results ?? [];
291291
if (!orderField) return raw;
292292
return [...raw].sort((a, b) => Number(a[orderField] ?? 0) - Number(b[orderField] ?? 0));
293-
}, [listQuery.data.items, orderField]);
293+
}, [listQuery.data.results, orderField]);
294294

295295
const handleDelete = (item: ResourceRow) => {
296296
const itemLabel = firstTranslatedName(columns, item);
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Box, FormControl, InputLabel, MenuItem, Pagination, Select, Stack } from "@mui/material";
2+
import { FC } from "react";
3+
import { useSearchParams } from "react-router-dom";
4+
5+
import { PAGE_SIZE_OPTIONS, usePaginationParams } from "@apps/pyconkr-admin/components/elements/pagination";
6+
7+
// 페이지네이션 컨트롤(페이지 이동 + 페이지당 개수). URL의 page/page_size를 직접 갱신한다.
8+
export const ListPagination: FC<{ totalCount: number }> = ({ totalCount }) => {
9+
const [searchParams, setSearchParams] = useSearchParams();
10+
const { page, pageSize } = usePaginationParams();
11+
const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
12+
13+
const handlePageChange = (_: unknown, newPage: number) => {
14+
const next = new URLSearchParams(searchParams);
15+
next.set("page", String(newPage));
16+
setSearchParams(next, { replace: true });
17+
window.scrollTo({ top: 0, behavior: "smooth" });
18+
};
19+
20+
const handlePageSizeChange = (newSize: number) => {
21+
const next = new URLSearchParams(searchParams);
22+
next.set("page_size", String(newSize));
23+
next.delete("page"); // page boundaries shift, so reset to 1
24+
setSearchParams(next, { replace: true });
25+
};
26+
27+
return (
28+
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mt: 2 }}>
29+
<Box sx={{ width: 140 }} />
30+
<Pagination count={totalPages} page={page} onChange={handlePageChange} showFirstButton showLastButton />
31+
<FormControl size="small" sx={{ width: 140 }}>
32+
<InputLabel>페이지당</InputLabel>
33+
<Select value={pageSize} label="페이지당" onChange={(e) => handlePageSizeChange(Number(e.target.value))}>
34+
{PAGE_SIZE_OPTIONS.map((n) => (
35+
<MenuItem key={n} value={n}>
36+
{n}
37+
</MenuItem>
38+
))}
39+
</Select>
40+
</FormControl>
41+
</Stack>
42+
);
43+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { useSearchParams } from "react-router-dom";
2+
3+
export const DEFAULT_PAGE_SIZE = 25;
4+
export const PAGE_SIZE_OPTIONS = [25, 50, 100, 200];
5+
export const PAGINATION_PARAM_KEYS = new Set(["page", "page_size"]);
6+
7+
// URL의 page/page_size를 파싱해 목록 쿼리 파라미터로 쓸 값을 돌려준다.
8+
export const usePaginationParams = () => {
9+
const [searchParams] = useSearchParams();
10+
const page = Math.max(1, Number(searchParams.get("page")) || 1);
11+
const pageSize = Math.max(1, Number(searchParams.get("page_size")) || DEFAULT_PAGE_SIZE);
12+
return { page, pageSize };
13+
};

apps/pyconkr-admin/src/components/layouts/admin_list.tsx

Lines changed: 8 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,26 @@
11
import {
22
useBackendAdminClient,
33
useFieldSelectablesQuery,
4-
useListAutoQuery,
4+
useListPaginatedQuery,
55
useOpenApiSchemaQuery,
66
useRemovePreparedMutation,
77
useSelectablesQueries,
88
} from "@frontend/common/hooks/useAdminAPI";
99
import { ChoicesResponse } from "@frontend/common/schemas/backendAdminAPI";
1010
import { extractQueryParameters } from "@frontend/common/utils";
1111
import { Add, Delete, Edit } from "@mui/icons-material";
12-
import {
13-
Box,
14-
Button,
15-
CircularProgress,
16-
FormControl,
17-
IconButton,
18-
InputLabel,
19-
MenuItem,
20-
Pagination,
21-
Select,
22-
Stack,
23-
Table,
24-
TableBody,
25-
TableCell,
26-
TableHead,
27-
TableRow,
28-
Typography,
29-
} from "@mui/material";
12+
import { Box, Button, CircularProgress, IconButton, Stack, Table, TableBody, TableCell, TableHead, TableRow, Typography } from "@mui/material";
3013
import { ErrorBoundary, Suspense } from "@suspensive/react";
3114
import { FC, type ReactNode, useMemo } from "react";
3215
import { Link, useNavigate, useSearchParams } from "react-router-dom";
3316

3417
import { AdminListFilter } from "@apps/pyconkr-admin/components/elements/admin_list_filter";
3518
import { BackendAdminSignInGuard } from "@apps/pyconkr-admin/components/elements/admin_signin_guard";
3619
import { ErrorFallback } from "@apps/pyconkr-admin/components/elements/error_fallback";
20+
import { ListPagination } from "@apps/pyconkr-admin/components/elements/list_pagination";
21+
import { PAGINATION_PARAM_KEYS, usePaginationParams } from "@apps/pyconkr-admin/components/elements/pagination";
3722
import { addErrorSnackbar, addSnackbar } from "@apps/pyconkr-admin/utils/snackbar";
3823

39-
const DEFAULT_PAGE_SIZE = 25;
40-
const PAGE_SIZE_OPTIONS = [25, 50, 100, 200];
41-
const PAGINATION_PARAM_KEYS = new Set(["page", "page_size"]);
42-
4324
type ListRowType = {
4425
id: string;
4526
str_repr: string;
@@ -83,15 +64,13 @@ const InnerAdminList: FC<AdminListProps> = ErrorBoundary.with(
8364
const backendAdminClient = useBackendAdminClient();
8465

8566
const allParams: Record<string, string> = Object.fromEntries(searchParams.entries());
86-
const page = Math.max(1, Number(allParams.page) || 1);
87-
const pageSize = Math.max(1, Number(allParams.page_size) || DEFAULT_PAGE_SIZE);
67+
const { page, pageSize } = usePaginationParams();
8868
// filterParams = user-facing filters only (page/page_size stripped); used for AdminListFilter UI state.
8969
const filterParams: Record<string, string> = Object.fromEntries(Object.entries(allParams).filter(([k]) => !PAGINATION_PARAM_KEYS.has(k)));
9070
// apiParams = filters + explicit pagination so non-paginated endpoints just ignore page/page_size.
9171
const apiParams: Record<string, string> = { ...filterParams, page: String(page), page_size: String(pageSize) };
92-
const listQuery = useListAutoQuery<ListRowType & Record<string, unknown>>(backendAdminClient, app, resource, apiParams);
93-
const items = listQuery.data.items;
94-
const pagination = listQuery.data.pagination;
72+
const listQuery = useListPaginatedQuery<ListRowType & Record<string, unknown>>(backendAdminClient, app, resource, apiParams);
73+
const { results: items, count: totalCount } = listQuery.data;
9574

9675
const openApiSchemaQuery = useOpenApiSchemaQuery(backendAdminClient);
9776
const queryParameters = useMemo(
@@ -124,22 +103,6 @@ const InnerAdminList: FC<AdminListProps> = ErrorBoundary.with(
124103
setSearchParams(merged, { replace: true });
125104
};
126105

127-
const handlePageChange = (_: unknown, newPage: number) => {
128-
const next = new URLSearchParams(searchParams);
129-
next.set("page", String(newPage));
130-
setSearchParams(next, { replace: true });
131-
window.scrollTo({ top: 0, behavior: "smooth" });
132-
};
133-
134-
const handlePageSizeChange = (newSize: number) => {
135-
const next = new URLSearchParams(searchParams);
136-
next.set("page_size", String(newSize));
137-
next.delete("page"); // page boundaries shift, so reset to 1
138-
setSearchParams(next, { replace: true });
139-
};
140-
141-
const totalPages = pagination ? Math.max(1, Math.ceil(pagination.count / pageSize)) : 1;
142-
143106
const detailPath = (id: string) => `/${app}/${resource}/${id}`;
144107
const hasCustomColumns = !!(columns && columns.length > 0);
145108

@@ -244,22 +207,7 @@ const InnerAdminList: FC<AdminListProps> = ErrorBoundary.with(
244207
))}
245208
</TableBody>
246209
</Table>
247-
{pagination && (
248-
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mt: 2 }}>
249-
<Box sx={{ width: 140 }} />
250-
<Pagination count={totalPages} page={page} onChange={handlePageChange} showFirstButton showLastButton />
251-
<FormControl size="small" sx={{ width: 140 }}>
252-
<InputLabel>페이지당</InputLabel>
253-
<Select value={pageSize} label="페이지당" onChange={(e) => handlePageSizeChange(Number(e.target.value))}>
254-
{PAGE_SIZE_OPTIONS.map((n) => (
255-
<MenuItem key={n} value={n}>
256-
{n}
257-
</MenuItem>
258-
))}
259-
</Select>
260-
</FormControl>
261-
</Stack>
262-
)}
210+
<ListPagination totalCount={totalCount} />
263211
</Stack>
264212
);
265213
}
Lines changed: 41 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,49 @@
1-
import { useBackendAdminClient, useListAutoQuery } from "@frontend/common/hooks/useAdminAPI";
2-
import { CircularProgress, Stack, Table, TableBody, TableCell, TableHead, TableRow, Typography } from "@mui/material";
3-
import { ErrorBoundary, Suspense } from "@suspensive/react";
1+
import { Typography } from "@mui/material";
42
import { FC } from "react";
53
import { Link } from "react-router-dom";
64

7-
import { BackendAdminSignInGuard } from "@apps/pyconkr-admin/components/elements/admin_signin_guard";
8-
import { ErrorFallback } from "@apps/pyconkr-admin/components/elements/error_fallback";
5+
import { AdminList, AdminListColumn } from "@apps/pyconkr-admin/components/layouts/admin_list";
96

10-
type ListRowType = {
11-
id: string;
12-
status: "approved" | "rejected" | "requested" | "cancelled";
13-
str_repr: string;
14-
created_at: string;
15-
updated_at: string;
16-
};
7+
const detailPath = (id: string) => `/participant_portal_api/modificationaudit/${id}`;
178

18-
const InnerAdminModificationAuditList: FC = ErrorBoundary.with(
19-
{ fallback: ErrorFallback },
20-
Suspense.with({ fallback: <CircularProgress /> }, () => {
21-
const backendAdminClient = useBackendAdminClient();
22-
const listQuery = useListAutoQuery<ListRowType>(backendAdminClient, "participant_portal_api", "modificationaudit");
23-
24-
return (
25-
<Stack sx={{ flexGrow: 1, width: "100%", minHeight: "100%" }}>
26-
<Typography variant="h5" children="수정 심사 목록" />
27-
<br />
28-
<Table>
29-
<TableHead>
30-
<TableRow>
31-
<TableCell sx={{ width: "25%" }}>ID</TableCell>
32-
<TableCell sx={{ width: "17.5%" }}>상태</TableCell>
33-
<TableCell sx={{ width: "40%" }}>이름</TableCell>
34-
<TableCell sx={{ width: "17.5%" }}>요청 시각</TableCell>
35-
</TableRow>
36-
</TableHead>
37-
<TableBody>
38-
{listQuery.data.items.map((item) => {
39-
const link = `/modification-audit/modification-audit/${item.id}`;
40-
const isRequested = item.status === "requested";
41-
return (
42-
<TableRow key={item.id}>
43-
<TableCell children={<Link to={link} children={item.id} />} />
44-
<TableCell>
45-
<Typography variant="body2" fontWeight={isRequested ? 700 : 400} color={isRequested ? "primary" : "textSecondary"}>
46-
{item.status}
47-
</Typography>
48-
</TableCell>
49-
<TableCell children={<Link to={link} children={item.str_repr} />} />
50-
<TableCell>{new Date(item.created_at).toLocaleString()}</TableCell>
51-
</TableRow>
52-
);
53-
})}
54-
</TableBody>
55-
</Table>
56-
</Stack>
57-
);
58-
})
59-
);
9+
const columns: AdminListColumn[] = [
10+
{ field: "id", header: "ID", width: "25%" },
11+
{
12+
field: "status",
13+
header: "상태",
14+
width: "17.5%",
15+
render: (row) => {
16+
const isRequested = row.status === "requested";
17+
return (
18+
<Typography variant="body2" fontWeight={isRequested ? 700 : 400} color={isRequested ? "primary" : "textSecondary"}>
19+
{String(row.status)}
20+
</Typography>
21+
);
22+
},
23+
},
24+
{
25+
field: "str_repr",
26+
header: "이름",
27+
width: "40%",
28+
render: (row) => <Link to={detailPath(String(row.id))}>{String(row.str_repr)}</Link>,
29+
},
30+
{
31+
field: "created_at",
32+
header: "요청 시각",
33+
width: "17.5%",
34+
render: (row) => new Date(String(row.created_at)).toLocaleString(),
35+
},
36+
];
6037

38+
// 수정 심사는 CRUD가 아닌 승인/반려 워크플로우라 생성/수정/삭제 액션을 숨기고, 상세는 전용 심사 페이지로 연결한다.
6139
export const AdminModificationAuditList: FC = () => (
62-
<BackendAdminSignInGuard>
63-
<InnerAdminModificationAuditList />
64-
</BackendAdminSignInGuard>
40+
<AdminList
41+
app="participant_portal_api"
42+
resource="modificationaudit"
43+
title="수정 심사 목록"
44+
columns={columns}
45+
hideCreateNew
46+
hideCreatedAt
47+
hideUpdatedAt
48+
/>
6549
);

apps/pyconkr-admin/src/components/pages/modification_audit/pages.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const InnerAdminModificationAuditEditor: FC = () => {
2020
const backendAdminClient = useBackendAdminClient();
2121
const { data } = useModificationAuditPreviewQuery<Record<string, string>>(backendAdminClient, id || "");
2222

23-
if (!data) return <Navigate to="/admin/modification-audit" replace />;
23+
if (!data) return <Navigate to="/participant_portal_api/modificationaudit" replace />;
2424

2525
const { modification_audit } = data;
2626
const { status, instance } = modification_audit;

apps/pyconkr-admin/src/components/pages/notification/send_history_create.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Fieldset } from "@frontend/common/components";
2-
import { useBackendAdminClient, useCreateMutation, useListAutoQuery, useRenderTemplateMutation } from "@frontend/common/hooks/useAdminAPI";
2+
import { useBackendAdminClient, useCreateMutation, useListPaginatedQuery, useRenderTemplateMutation } from "@frontend/common/hooks/useAdminAPI";
33
import { Add, Close, Delete, Send, Visibility } from "@mui/icons-material";
44
import {
55
Box,
@@ -208,9 +208,9 @@ const InnerAdminNotificationHistoryCreate: FC<AdminNotificationHistoryCreateProp
208208
const [templateContext, setTemplateContext] = useState<Record<string, string>>({});
209209
const [globalVarFlags, setGlobalVarFlags] = useState<Record<string, boolean>>({});
210210

211-
const templateListQuery = useListAutoQuery<NotificationTemplateSchema>(backendAdminClient, app, templateResource);
211+
const templateListQuery = useListPaginatedQuery<NotificationTemplateSchema>(backendAdminClient, app, templateResource, { page_size: "200" });
212212
const renderTemplateMutation = useRenderTemplateMutation(backendAdminClient, app, templateResource);
213-
const selectedTemplate = templateListQuery.data.items.find((t) => t.id === formData.template) ?? null;
213+
const selectedTemplate = templateListQuery.data.results.find((t) => t.id === formData.template) ?? null;
214214
const templateVariables = selectedTemplate?.template_variables ?? [];
215215

216216
const isGlobalVar = (varName: string) => globalVarFlags[varName] ?? !NotAppliableToAllRecipientsFieldList.includes(varName);
@@ -324,7 +324,7 @@ const InnerAdminNotificationHistoryCreate: FC<AdminNotificationHistoryCreateProp
324324
<MenuItem value="">
325325
<em>(없음)</em>
326326
</MenuItem>
327-
{templateListQuery.data.items.map((t) => (
327+
{templateListQuery.data.results.map((t) => (
328328
<MenuItem key={t.id} value={t.id}>
329329
{t.str_repr}
330330
</MenuItem>

apps/pyconkr-admin/src/components/pages/presentation/editor.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
useBackendAdminClient,
44
useCreateMutation,
55
useFieldSelectablesQuery,
6-
useListAutoQuery,
6+
useListPaginatedQuery,
77
useRemovePreparedMutation,
88
useSchemaQuery,
99
useUpdatePreparedMutation,
@@ -291,8 +291,8 @@ export const AdminPresentationEditor: FC = ErrorBoundary.with(
291291
const { data: speakerJsonSchema } = useSchemaQuery(...speakerQueryParams);
292292
const speakerChoices = useFieldSelectablesQuery(...speakerQueryParams);
293293
const {
294-
data: { items: speakerInitialData },
295-
} = useListAutoQuery<PresentationSpeaker>(...speakerQueryParams, { presentation });
294+
data: { results: speakerInitialData },
295+
} = useListPaginatedQuery<PresentationSpeaker>(...speakerQueryParams, { presentation });
296296
const speakers = speakerInitialData.map((s) => ({ ...s, trackId: s.id || Math.random().toString(36).substring(2, 15) }));
297297

298298
const scheduleQueryParams = [backendAdminAPIClient, "event", "roomschedule"] as const;
@@ -302,8 +302,8 @@ export const AdminPresentationEditor: FC = ErrorBoundary.with(
302302
const { data: scheduleJsonSchema } = useSchemaQuery(...scheduleQueryParams);
303303
const scheduleChoices = useFieldSelectablesQuery(...scheduleQueryParams);
304304
const {
305-
data: { items: scheduleInitialData },
306-
} = useListAutoQuery<Schedule>(...scheduleQueryParams, { presentation });
305+
data: { results: scheduleInitialData },
306+
} = useListPaginatedQuery<Schedule>(...scheduleQueryParams, { presentation });
307307
const schedules = scheduleInitialData.map((s) => ({ ...s, trackId: s.id || Math.random().toString(36).substring(2, 15) }));
308308

309309
useMemo(() => {

0 commit comments

Comments
 (0)