diff --git a/devfront/src/features/clients/ClientsPage.tsx b/devfront/src/features/clients/ClientsPage.tsx index 630b7ade..aed8bb51 100644 --- a/devfront/src/features/clients/ClientsPage.tsx +++ b/devfront/src/features/clients/ClientsPage.tsx @@ -82,6 +82,7 @@ type RecentClientChange = { const recentClientChangesInitialCount = 5; const recentClientChangesBatchSize = 5; +const clientListPreviewCount = 5; const recentClientActions = new Set([ "CREATE_CLIENT", @@ -316,6 +317,7 @@ function ClientsPage() { const [isRequestModalOpen, setIsRequestModalOpen] = useState(false); const [isRecentChangesGuideOpen, setIsRecentChangesGuideOpen] = useState(false); + const [isClientListExpanded, setIsClientListExpanded] = useState(false); const [visibleRecentClientChangesCount, setVisibleRecentClientChangesCount] = useState(recentClientChangesInitialCount); const [sortConfig, setSortConfig] = @@ -425,6 +427,14 @@ function ClientsPage() { const authFailures = statsData?.auth_failures_24h ?? 0; const hasFilterResult = filteredClients.length > 0; const isFilteredOut = clients.length > 0 && !hasFilterResult; + const visibleClients = useMemo(() => { + if (isClientListExpanded) { + return filteredClients; + } + + return filteredClients.slice(0, clientListPreviewCount); + }, [filteredClients, isClientListExpanded]); + const canToggleClientList = filteredClients.length > clientListPreviewCount; const currentTenant = tenants?.find( (tenant) => tenant.id === tenantId || tenant.slug === companyCode, ); @@ -784,15 +794,20 @@ function ClientsPage() { /> -
+
{stats.map((item) => ( - - + + {t(item.labelKey, item.labelFallback)} -
- {item.value} +
+ + {item.value} + @@ -954,7 +969,7 @@ function ClientsPage() { )} - {filteredClients.map((client) => ( + {visibleClients.map((client) => (
+ {canToggleClientList ? ( +
+ +
+ ) : null}
diff --git a/devfront/tests/clients.spec.ts b/devfront/tests/clients.spec.ts index 157e10d2..9777876f 100644 --- a/devfront/tests/clients.spec.ts +++ b/devfront/tests/clients.spec.ts @@ -100,6 +100,59 @@ test("clients page shows recent RP changes", async ({ page }) => { ).toBeVisible(); }); +test("clients page shows only five apps by default and expands with more button", async ({ + page, +}) => { + await seedAuth(page, "super_admin"); + const clients = Array.from({ length: 6 }, (_, index) => + makeClient(`client-${index + 1}`, { + name: `Preview App ${index + 1}`, + createdAt: new Date( + Date.UTC(2026, 2, 3, 9, 10 - index, 0), + ).toISOString(), + }), + ); + + await installDevApiMock(page, { + clients, + consents: [] as Consent[], + auditLogs: [] as AuditLog[], + auditLogsByCursor: undefined, + }); + + await page.goto("/clients"); + await expect( + page.getByRole("heading", { name: "연동 앱 목록" }), + ).toBeVisible(); + await expect( + page.locator("table").first().locator("tbody tr").filter({ + hasText: /Preview App \d/, + }), + ).toHaveCount(5); + await expect( + page.getByText("Preview App 6", { exact: true }), + ).not.toBeVisible(); + + const moreButton = page.getByRole("button", { + name: "연동 앱 목록 더보기", + }); + await expect(moreButton).toBeVisible(); + await expect(moreButton).toHaveCount(1); + await moreButton.click(); + + await expect( + page.locator("table").first().locator("tbody tr").filter({ + hasText: /Preview App \d/, + }), + ).toHaveCount(6); + await expect( + page.getByText("Preview App 6", { exact: true }), + ).toBeVisible(); + await expect( + page.getByRole("button", { name: "연동 앱 목록 더보기" }), + ).toHaveCount(0); +}); + test("clients page shows user-delete relation cleanup in recent changes", async ({ page, }) => {