manage license app launch policies
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-10 13:14:23 +09:00
parent 93fc7b7b4e
commit ac0c8eb29d
7 changed files with 620 additions and 97 deletions
+38 -11
View File
@@ -48,7 +48,8 @@ export function DataTableCard<T>({
pageSize = 8,
minWidth = 980,
loading = false,
emptyMessage = '데이터가 없습니다.'
emptyMessage = '데이터가 없습니다.',
serverPagination
}: {
title: string;
description?: string;
@@ -62,12 +63,23 @@ export function DataTableCard<T>({
minWidth?: number;
loading?: boolean;
emptyMessage?: string;
serverPagination?: {
page: number;
total: number;
onPageChange: (page: number) => void;
onSearchChange?: (keyword: string) => void;
filterValues?: Record<string, string>;
onFilterChange?: (key: string, value: string) => void;
};
}) {
const [search, setSearch] = useState('');
const [filterValues, setFilterValues] = useState<Record<string, string>>({});
const [page, setPage] = useState(1);
const serverMode = Boolean(serverPagination);
const activeFilterValues = serverPagination?.filterValues ?? filterValues;
const filteredRows = useMemo(() => {
if (serverMode) return rows;
const normalizedSearch = search.trim().toLowerCase();
return rows.filter((row) => {
@@ -75,24 +87,37 @@ export function DataTableCard<T>({
? getSearchText(row).toLowerCase().includes(normalizedSearch)
: true;
const matchesFilters = filters.every((filter) => {
const selected = filterValues[filter.key];
const selected = activeFilterValues[filter.key];
return !selected || filter.getValue(row) === selected;
});
return matchesSearch && matchesFilters;
});
}, [filterValues, filters, getSearchText, rows, search]);
}, [activeFilterValues, filters, getSearchText, rows, search, serverMode]);
const totalPages = Math.max(1, Math.ceil(filteredRows.length / pageSize));
const currentPage = Math.min(page, totalPages);
const pageRows = filteredRows.slice((currentPage - 1) * pageSize, currentPage * pageSize);
const totalCount = serverPagination?.total ?? filteredRows.length;
const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
const currentPage = Math.min(serverPagination?.page ?? page, totalPages);
const pageRows = serverMode ? rows : filteredRows.slice((currentPage - 1) * pageSize, currentPage * pageSize);
const flexibleColumnIndex = columns[0]?.key === 'no' && columns.length > 1 ? 1 : -1;
const updateFilter = (key: string, value: string) => {
if (serverPagination?.onFilterChange) {
serverPagination.onFilterChange(key, value);
return;
}
setFilterValues((prev) => ({ ...prev, [key]: value }));
setPage(1);
};
const movePage = (nextPage: number) => {
if (serverPagination) {
serverPagination.onPageChange(nextPage);
return;
}
setPage(nextPage);
};
return (
<Card className="min-w-0 overflow-hidden">
<div className="flex flex-col gap-3 border-b border-border px-5 py-3 lg:flex-row lg:items-center lg:justify-between">
@@ -108,7 +133,9 @@ export function DataTableCard<T>({
placeholder={searchPlaceholder}
value={search}
onChange={(event) => {
setSearch(event.target.value);
const value = event.target.value;
setSearch(value);
serverPagination?.onSearchChange?.(value);
setPage(1);
}}
/>
@@ -120,7 +147,7 @@ export function DataTableCard<T>({
{filter.label}
<select
className="h-9 rounded-md border border-input bg-background px-3 text-sm font-medium text-foreground outline-none focus:ring-2 focus:ring-ring"
value={filterValues[filter.key] ?? ''}
value={activeFilterValues[filter.key] ?? ''}
onChange={(event) => updateFilter(filter.key, event.target.value)}
>
<option value="">전체</option>
@@ -210,14 +237,14 @@ export function DataTableCard<T>({
</div>
<div className="flex items-center justify-between gap-4 border-t border-border px-5 py-3 text-sm">
<span className="text-xs text-muted-foreground">
{loading ? '불러오는 중' : `총 ${filteredRows.length}건 · ${currentPage} / ${totalPages}`}
{loading ? '불러오는 중' : `총 ${totalCount}건 · ${currentPage} / ${totalPages}`}
</span>
<div className="flex gap-2">
<Button
variant="outline"
className="h-8 px-3 text-[13px]"
disabled={currentPage <= 1}
onClick={() => setPage((prev) => Math.max(1, prev - 1))}
onClick={() => movePage(Math.max(1, currentPage - 1))}
>
<ChevronLeft className="size-4" />
이전
@@ -226,7 +253,7 @@ export function DataTableCard<T>({
variant="outline"
className="h-8 px-3 text-[13px]"
disabled={currentPage >= totalPages}
onClick={() => setPage((prev) => Math.min(totalPages, prev + 1))}
onClick={() => movePage(Math.min(totalPages, currentPage + 1))}
>
다음
<ChevronRight className="size-4" />