296 lines
9.8 KiB
TypeScript
296 lines
9.8 KiB
TypeScript
import Link from "next/link";
|
|
import {
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
ChevronsLeft,
|
|
ChevronsRight,
|
|
Search,
|
|
} from "lucide-react";
|
|
|
|
import { Button, buttonVariants } from "@/components/ui/button";
|
|
import { MaestrosTable } from "@/components/data-table/maestros-table";
|
|
import { AutoSubmitSelect } from "@/components/forms/auto-submit-select";
|
|
import {
|
|
ACCOUNT_TYPE_LABELS,
|
|
ACCOUNT_TYPES,
|
|
ACTIVATE_STATUS_LABELS,
|
|
ACTIVATE_STATUSES,
|
|
} from "@/lib/constants";
|
|
import { getMaestros, type RawSearchParams } from "@/lib/maestros";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
type MaestrosPageProps = {
|
|
searchParams: Promise<RawSearchParams>;
|
|
};
|
|
|
|
const activateStatusOptions = [
|
|
ACTIVATE_STATUSES.PENDING,
|
|
ACTIVATE_STATUSES.TRIAL,
|
|
ACTIVATE_STATUSES.ACTIVE,
|
|
ACTIVATE_STATUSES.CANCELED,
|
|
] as const;
|
|
|
|
const accountTypeOptions = [
|
|
ACCOUNT_TYPES.BASIC_20,
|
|
ACCOUNT_TYPES.STANDARD_50,
|
|
ACCOUNT_TYPES.PRO_100,
|
|
ACCOUNT_TYPES.SCHOOL_500,
|
|
ACCOUNT_TYPES.SCHOOL_1000,
|
|
] as const;
|
|
|
|
const sortOptions = [
|
|
{ value: "id-desc", label: "ID 최신순" },
|
|
{ value: "id-asc", label: "ID 오래된순" },
|
|
{ value: "joinedAt-desc", label: "가입일 최신순" },
|
|
{ value: "joinedAt-asc", label: "가입일 오래된순" },
|
|
{ value: "availableAt-desc", label: "사용 가능일 최신순" },
|
|
{ value: "availableAt-asc", label: "사용 가능일 오래된순" },
|
|
] as const;
|
|
|
|
export default async function MaestrosPage({
|
|
searchParams,
|
|
}: MaestrosPageProps) {
|
|
const params = await searchParams;
|
|
const result = await getMaestros(params);
|
|
const paginationItems = buildPaginationItems(result.page, result.totalPages);
|
|
const firstVisiblePage = paginationItems[0] ?? 1;
|
|
const lastVisiblePage = paginationItems.at(-1) ?? result.totalPages;
|
|
const hasFirstPage = paginationItems.includes(1);
|
|
const hasLastPage = paginationItems.includes(result.totalPages);
|
|
const hasPreviousPageGroup = firstVisiblePage > 1;
|
|
const hasNextPageGroup = lastVisiblePage < result.totalPages;
|
|
|
|
return (
|
|
<section className="space-y-5">
|
|
<div className="flex flex-col gap-1 sm:flex-row sm:items-end sm:justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-semibold tracking-normal">마에스트로</h1>
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
|
전체 {result.totalCount.toLocaleString()}명 중 {result.page} /{" "}
|
|
{result.totalPages} 페이지
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<form
|
|
className="grid grid-cols-1 gap-3 rounded-lg border bg-background p-4 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-[minmax(220px,1fr)_160px_180px_190px_110px_auto]"
|
|
method="get"
|
|
>
|
|
<label className="min-w-0 space-y-1.5">
|
|
<span className="text-xs font-medium text-muted-foreground">
|
|
검색
|
|
</span>
|
|
<div className="relative min-w-0">
|
|
<Search
|
|
className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"
|
|
aria-hidden="true"
|
|
/>
|
|
<input
|
|
className="h-9 w-full min-w-0 rounded-md border bg-background pl-9 pr-3 text-sm outline-none transition-colors focus:border-ring focus:ring-3 focus:ring-ring/30"
|
|
defaultValue={result.q}
|
|
name="q"
|
|
placeholder="이름, 이메일, ID"
|
|
type="search"
|
|
/>
|
|
</div>
|
|
</label>
|
|
|
|
<label className="min-w-0 space-y-1.5">
|
|
<span className="text-xs font-medium text-muted-foreground">
|
|
상태
|
|
</span>
|
|
<AutoSubmitSelect
|
|
className="h-9 w-full min-w-0 rounded-md border bg-background px-3 text-sm outline-none transition-colors focus:border-ring focus:ring-3 focus:ring-ring/30"
|
|
name="activateStatus"
|
|
value={result.activateStatus ?? "all"}
|
|
>
|
|
<option value="all">전체</option>
|
|
{activateStatusOptions.map((status) => (
|
|
<option key={status} value={status}>
|
|
{ACTIVATE_STATUS_LABELS[status]}
|
|
</option>
|
|
))}
|
|
</AutoSubmitSelect>
|
|
</label>
|
|
|
|
<label className="min-w-0 space-y-1.5">
|
|
<span className="text-xs font-medium text-muted-foreground">
|
|
계정 유형
|
|
</span>
|
|
<AutoSubmitSelect
|
|
className="h-9 w-full min-w-0 rounded-md border bg-background px-3 text-sm outline-none transition-colors focus:border-ring focus:ring-3 focus:ring-ring/30"
|
|
name="accountType"
|
|
value={result.accountType ?? "all"}
|
|
>
|
|
<option value="all">전체</option>
|
|
{accountTypeOptions.map((accountType) => (
|
|
<option key={accountType} value={accountType}>
|
|
{ACCOUNT_TYPE_LABELS[accountType]}
|
|
</option>
|
|
))}
|
|
</AutoSubmitSelect>
|
|
</label>
|
|
|
|
<label className="min-w-0 space-y-1.5">
|
|
<span className="text-xs font-medium text-muted-foreground">
|
|
정렬
|
|
</span>
|
|
<AutoSubmitSelect
|
|
className="h-9 w-full min-w-0 rounded-md border bg-background px-3 text-sm outline-none transition-colors focus:border-ring focus:ring-3 focus:ring-ring/30"
|
|
name="sort"
|
|
value={result.sort}
|
|
>
|
|
{sortOptions.map((option) => (
|
|
<option key={option.value} value={option.value}>
|
|
{option.label}
|
|
</option>
|
|
))}
|
|
</AutoSubmitSelect>
|
|
</label>
|
|
|
|
<label className="min-w-0 space-y-1.5">
|
|
<span className="text-xs font-medium text-muted-foreground">
|
|
페이지 크기
|
|
</span>
|
|
<AutoSubmitSelect
|
|
className="h-9 w-full min-w-0 rounded-md border bg-background px-3 text-sm outline-none transition-colors focus:border-ring focus:ring-3 focus:ring-ring/30"
|
|
name="pageSize"
|
|
value={result.pageSize}
|
|
>
|
|
{[10, 20, 50, 100].map((pageSize) => (
|
|
<option key={pageSize} value={pageSize}>
|
|
{pageSize}
|
|
</option>
|
|
))}
|
|
</AutoSubmitSelect>
|
|
</label>
|
|
|
|
<div className="flex min-w-0 flex-wrap items-end gap-2 sm:col-span-2 xl:col-span-1">
|
|
<Button className="h-9" type="submit">
|
|
적용
|
|
</Button>
|
|
<Link
|
|
className={buttonVariants({
|
|
variant: "outline",
|
|
size: "default",
|
|
className: "h-9",
|
|
})}
|
|
href="/maestros"
|
|
>
|
|
초기화
|
|
</Link>
|
|
</div>
|
|
</form>
|
|
|
|
<MaestrosTable data={result.items} />
|
|
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<p className="text-sm text-muted-foreground">
|
|
페이지당 {result.pageSize}개 표시
|
|
</p>
|
|
<nav
|
|
aria-label="마에스트로 목록 페이지"
|
|
className="flex flex-wrap items-center gap-1.5"
|
|
>
|
|
{!hasFirstPage ? (
|
|
<Link
|
|
aria-label="처음 페이지"
|
|
className={buttonVariants({
|
|
variant: "outline",
|
|
size: "icon-sm",
|
|
})}
|
|
href={buildPageHref(params, 1)}
|
|
>
|
|
<ChevronsLeft className="size-4" aria-hidden="true" />
|
|
</Link>
|
|
) : null}
|
|
|
|
{hasPreviousPageGroup ? (
|
|
<Link
|
|
aria-label="이전 5페이지"
|
|
className={buttonVariants({
|
|
variant: "outline",
|
|
size: "icon-sm",
|
|
})}
|
|
href={buildPageHref(params, Math.max(1, result.page - 5))}
|
|
>
|
|
<ChevronLeft className="size-4" aria-hidden="true" />
|
|
</Link>
|
|
) : null}
|
|
|
|
{paginationItems.map((item) => (
|
|
<Link
|
|
aria-current={item === result.page ? "page" : undefined}
|
|
className={cn(
|
|
buttonVariants({
|
|
variant: item === result.page ? "default" : "outline",
|
|
size: "sm",
|
|
}),
|
|
"min-w-8 px-2"
|
|
)}
|
|
href={buildPageHref(params, item)}
|
|
key={item}
|
|
>
|
|
{item}
|
|
</Link>
|
|
))}
|
|
|
|
{hasNextPageGroup ? (
|
|
<Link
|
|
aria-label="다음 5페이지"
|
|
className={buttonVariants({
|
|
variant: "outline",
|
|
size: "icon-sm",
|
|
})}
|
|
href={buildPageHref(
|
|
params,
|
|
Math.min(result.totalPages, result.page + 5)
|
|
)}
|
|
>
|
|
<ChevronRight className="size-4" aria-hidden="true" />
|
|
</Link>
|
|
) : null}
|
|
|
|
{!hasLastPage ? (
|
|
<Link
|
|
aria-label="끝 페이지"
|
|
className={buttonVariants({
|
|
variant: "outline",
|
|
size: "icon-sm",
|
|
})}
|
|
href={buildPageHref(params, result.totalPages)}
|
|
>
|
|
<ChevronsRight className="size-4" aria-hidden="true" />
|
|
</Link>
|
|
) : null}
|
|
</nav>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function buildPageHref(rawParams: RawSearchParams, page: number) {
|
|
const params = new URLSearchParams();
|
|
|
|
Object.entries(rawParams).forEach(([key, value]) => {
|
|
const firstValue = Array.isArray(value) ? value[0] : value;
|
|
|
|
if (firstValue && key !== "page") {
|
|
params.set(key, firstValue);
|
|
}
|
|
});
|
|
params.set("page", String(Math.max(1, page)));
|
|
|
|
return `/maestros?${params.toString()}`;
|
|
}
|
|
|
|
function buildPaginationItems(currentPage: number, totalPages: number) {
|
|
const startPage = Math.max(1, currentPage - 2);
|
|
const endPage = Math.min(totalPages, currentPage + 2);
|
|
|
|
return Array.from(
|
|
{ length: endPage - startPage + 1 },
|
|
(_, index) => startPage + index
|
|
);
|
|
}
|