1
0
forked from baron/baron-sso

검색/필터 바 shell 공통화

This commit is contained in:
2026-05-14 14:42:29 +09:00
parent b3c360c54f
commit faffb6dc05
6 changed files with 398 additions and 357 deletions

View File

@@ -34,6 +34,7 @@ import {
commonTableShellClass, commonTableShellClass,
commonTableViewportClass, commonTableViewportClass,
} from "../../../../common/ui/table"; } from "../../../../common/ui/table";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { import {
Card, Card,
@@ -426,47 +427,53 @@ function UserListPage() {
)} )}
actions={ actions={
<> <>
<div className="mr-2 flex items-center gap-2"> <SearchFilterBar
<div className="relative w-48"> primary={
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" /> <>
<Input <div className="relative w-48">
placeholder={t( <Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
"ui.admin.users.list.search_placeholder", <Input
"이름 또는 이메일 검색...", placeholder={t(
)} "ui.admin.users.list.search_placeholder",
className="h-9 pl-9" "이름 또는 이메일 검색...",
value={searchDraft} )}
onChange={(e) => setSearchDraft(e.target.value)} className="h-9 pl-9"
onKeyDown={handleKeyDown} value={searchDraft}
/> onChange={(e) => setSearchDraft(e.target.value)}
</div> onKeyDown={handleKeyDown}
/>
</div>
<select <select
className="flex h-9 w-[160px] rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50" className="flex h-9 w-[160px] rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
value={selectedCompany} value={selectedCompany}
onChange={(e) => { onChange={(e) => {
setSelectedCompany(e.target.value); setSelectedCompany(e.target.value);
setPage(1); setPage(1);
}} }}
disabled={profile?.role === "tenant_admin"} disabled={profile?.role === "tenant_admin"}
> >
<option value="">{t("ui.common.all", "전체 테넌트")}</option> <option value="">
{tenants.map((t) => ( {t("ui.common.all", "전체 테넌트")}
<option key={t.id} value={t.slug}> </option>
{t.name} {tenants.map((t) => (
</option> <option key={t.id} value={t.slug}>
))} {t.name}
</select> </option>
))}
</select>
<Button <Button
variant="secondary" variant="secondary"
size="sm" size="sm"
onClick={handleSearch} onClick={handleSearch}
className="h-9" className="h-9"
> >
{t("ui.common.search", "검색")} {t("ui.common.search", "검색")}
</Button> </Button>
</div> </>
}
/>
<Button <Button
variant="outline" variant="outline"

View File

@@ -30,6 +30,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "../../components/ui/table"; } from "../../components/ui/table";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { PageHeader } from "../../../../common/core/components/page"; import { PageHeader } from "../../../../common/core/components/page";
import type { DevAuditLog } from "../../lib/devApi"; import type { DevAuditLog } from "../../lib/devApi";
import { fetchDevAuditLogs } from "../../lib/devApi"; import { fetchDevAuditLogs } from "../../lib/devApi";
@@ -227,49 +228,55 @@ function AuditLogsPage() {
<Card className="glass-panel"> <Card className="glass-panel">
<CardContent className="space-y-4 pt-6"> <CardContent className="space-y-4 pt-6">
<form <SearchFilterBar
onSubmit={(e) => { primary={
e.preventDefault(); <form
query.refetch(); onSubmit={(e) => {
}} e.preventDefault();
className="grid gap-2 md:grid-cols-[1fr,1fr,180px]" query.refetch();
> }}
<div className="relative"> className="grid flex-1 gap-2 md:grid-cols-[1fr,1fr,180px]"
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> >
<Input <div className="relative">
className="pl-10" <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
value={searchClientId} <Input
onChange={(e) => setSearchClientId(e.target.value)} className="pl-10"
placeholder={t( value={searchClientId}
"ui.dev.audit.filter.client_id", onChange={(e) => setSearchClientId(e.target.value)}
"Filter by Client ID", placeholder={t(
)} "ui.dev.audit.filter.client_id",
/> "Filter by Client ID",
</div> )}
<Input />
value={searchAction} </div>
onChange={(e) => setSearchAction(e.target.value.toUpperCase())} <Input
placeholder={t( value={searchAction}
"ui.dev.audit.filter.action", onChange={(e) =>
"Filter by Action (e.g. ROTATE_SECRET)", setSearchAction(e.target.value.toUpperCase())
)} }
/> placeholder={t(
<select "ui.dev.audit.filter.action",
className="h-10 rounded-md border border-input bg-background px-3 text-sm" "Filter by Action (e.g. ROTATE_SECRET)",
value={statusFilter} )}
onChange={(e) => setStatusFilter(e.target.value)} />
> <select
<option value="all"> className="h-10 rounded-md border border-input bg-background px-3 text-sm"
{t("ui.dev.audit.filter.status_all", "All Status")} value={statusFilter}
</option> onChange={(e) => setStatusFilter(e.target.value)}
<option value="success"> >
{t("ui.common.status.success", "Success")} <option value="all">
</option> {t("ui.dev.audit.filter.status_all", "All Status")}
<option value="failure"> </option>
{t("ui.common.status.failure", "Failure")} <option value="success">
</option> {t("ui.common.status.success", "Success")}
</select> </option>
</form> <option value="failure">
{t("ui.common.status.failure", "Failure")}
</option>
</select>
</form>
}
/>
<div <div
className={ className={

View File

@@ -9,6 +9,7 @@ import {
sortableTableHeadBaseClassName, sortableTableHeadBaseClassName,
sortableTableHeaderClassName, sortableTableHeaderClassName,
} from "../../../../common/core/components/sort"; } from "../../../../common/core/components/sort";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { PageHeader } from "../../../../common/core/components/page"; import { PageHeader } from "../../../../common/core/components/page";
import { import {
type SortConfig, type SortConfig,
@@ -283,8 +284,8 @@ function ClientsPage() {
<Card className="glass-panel"> <Card className="glass-panel">
<CardHeader className="pb-4 pt-6"> <CardHeader className="pb-4 pt-6">
<div className="flex flex-col gap-3"> <SearchFilterBar
<div className="flex flex-col gap-3 md:flex-row md:items-center"> primary={
<div className="relative flex-1"> <div className="relative flex-1">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input <Input
@@ -297,7 +298,9 @@ function ClientsPage() {
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
/> />
</div> </div>
<div className="flex items-center gap-2"> }
actions={
<>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
@@ -324,65 +327,67 @@ function ClientsPage() {
{t("ui.dev.clients.badge.dev_session", "DevFront 세션")} {t("ui.dev.clients.badge.dev_session", "DevFront 세션")}
</Badge> </Badge>
</div> </div>
</div> </>
</div> }
advancedOpen={isAdvancedFilterOpen}
{isAdvancedFilterOpen && ( advanced={
<div className="flex flex-wrap items-center gap-6 rounded-lg bg-secondary/30 p-4 border border-border/40 animate-in fade-in slide-in-from-top-2 duration-200"> <>
<div className="flex items-center gap-2"> <div className="flex flex-wrap items-center gap-6">
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground whitespace-nowrap"> <div className="flex items-center gap-2">
{t("ui.dev.clients.filter.type_label", "Type:")} <span className="text-xs font-bold uppercase tracking-wider text-muted-foreground whitespace-nowrap">
</span> {t("ui.dev.clients.filter.type_label", "Type:")}
<select </span>
className="h-9 rounded-md border border-input bg-background px-3 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30 min-w-[140px]" <select
value={typeFilter} className="h-9 min-w-[140px] rounded-md border border-input bg-background px-3 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30"
onChange={(e) => setTypeFilter(e.target.value)} value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
>
<option value="all">
{t("ui.dev.clients.filter.type_all", "모든 유형")}
</option>
<option value="private">
{t("ui.dev.clients.type.private", "Server side App")}
</option>
<option value="pkce">
{t("ui.dev.clients.type.pkce", "PKCE")}
</option>
</select>
</div>
<div className="flex items-center gap-2">
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground whitespace-nowrap">
{t("ui.dev.clients.consents.status_label", "Status:")}
</span>
<select
className="h-9 min-w-[140px] rounded-md border border-input bg-background px-3 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
>
<option value="all">
{t("ui.dev.clients.filter.status_all", "모든 상태")}
</option>
<option value="active">
{t("ui.common.status.active", "Active")}
</option>
<option value="inactive">
{t("ui.common.status.inactive", "Inactive")}
</option>
</select>
</div>
<Button
variant="ghost"
size="sm"
className="ml-auto text-xs text-muted-foreground"
onClick={() => {
setTypeFilter("all");
setStatusFilter("all");
}}
> >
<option value="all"> {t("ui.common.reset", "초기화")}
{t("ui.dev.clients.filter.type_all", "모든 유형")} </Button>
</option>
<option value="private">
{t("ui.dev.clients.type.private", "Server side App")}
</option>
<option value="pkce">
{t("ui.dev.clients.type.pkce", "PKCE")}
</option>
</select>
</div> </div>
<div className="flex items-center gap-2"> </>
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground whitespace-nowrap"> }
{t("ui.dev.clients.consents.status_label", "Status:")} />
</span>
<select
className="h-9 rounded-md border border-input bg-background px-3 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30 min-w-[140px]"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
>
<option value="all">
{t("ui.dev.clients.filter.status_all", "모든 상태")}
</option>
<option value="active">
{t("ui.common.status.active", "Active")}
</option>
<option value="inactive">
{t("ui.common.status.inactive", "Inactive")}
</option>
</select>
</div>
<Button
variant="ghost"
size="sm"
className="text-xs text-muted-foreground ml-auto"
onClick={() => {
setTypeFilter("all");
setStatusFilter("all");
}}
>
{t("ui.common.reset", "초기화")}
</Button>
</div>
)}
</div>
</CardHeader> </CardHeader>
<CardContent className="pt-0"> <CardContent className="pt-0">
<div className="grid gap-4 md:grid-cols-3"> <div className="grid gap-4 md:grid-cols-3">

View File

@@ -28,6 +28,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "../../components/ui/table"; } from "../../components/ui/table";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import type { DevAuditLog } from "../../lib/devApi"; import type { DevAuditLog } from "../../lib/devApi";
import { fetchDevAuditLogs } from "../../lib/devApi"; import { fetchDevAuditLogs } from "../../lib/devApi";
import { t } from "../../lib/i18n"; import { t } from "../../lib/i18n";
@@ -227,43 +228,55 @@ function AuditLogsPage() {
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid gap-2 md:grid-cols-[1fr,1fr,180px]"> <SearchFilterBar
<div className="relative"> primary={
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <form
<Input onSubmit={(e) => {
className="pl-10" e.preventDefault();
value={searchClientId} query.refetch();
onChange={(e) => setSearchClientId(e.target.value)} }}
placeholder={t( className="grid flex-1 gap-2 md:grid-cols-[1fr,1fr,180px]"
"ui.dev.audit.filter.client_id", >
"Filter by Client ID", <div className="relative">
)} <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
/> <Input
</div> className="pl-10"
<Input value={searchClientId}
value={searchAction} onChange={(e) => setSearchClientId(e.target.value)}
onChange={(e) => setSearchAction(e.target.value.toUpperCase())} placeholder={t(
placeholder={t( "ui.dev.audit.filter.client_id",
"ui.dev.audit.filter.action", "Filter by Client ID",
"Filter by Action (e.g. ROTATE_SECRET)", )}
)} />
/> </div>
<select <Input
className="h-10 rounded-md border border-input bg-background px-3 text-sm" value={searchAction}
value={statusFilter} onChange={(e) =>
onChange={(e) => setStatusFilter(e.target.value)} setSearchAction(e.target.value.toUpperCase())
> }
<option value="all"> placeholder={t(
{t("ui.dev.audit.filter.status_all", "All Status")} "ui.dev.audit.filter.action",
</option> "Filter by Action (e.g. ROTATE_SECRET)",
<option value="success"> )}
{t("ui.common.status.success", "Success")} />
</option> <select
<option value="failure"> className="h-10 rounded-md border border-input bg-background px-3 text-sm"
{t("ui.common.status.failure", "Failure")} value={statusFilter}
</option> onChange={(e) => setStatusFilter(e.target.value)}
</select> >
</div> <option value="all">
{t("ui.dev.audit.filter.status_all", "All Status")}
</option>
<option value="success">
{t("ui.common.status.success", "Success")}
</option>
<option value="failure">
{t("ui.common.status.failure", "Failure")}
</option>
</select>
</form>
}
/>
<Table className="table-fixed"> <Table className="table-fixed">
<TableHeader> <TableHeader>

View File

@@ -26,6 +26,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "../../components/ui/table"; } from "../../components/ui/table";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { fetchClient, fetchConsents, revokeConsent } from "../../lib/devApi"; import { fetchClient, fetchConsents, revokeConsent } from "../../lib/devApi";
import { t } from "../../lib/i18n"; import { t } from "../../lib/i18n";
import { cn } from "../../lib/utils"; import { cn } from "../../lib/utils";
@@ -235,8 +236,8 @@ function ClientConsentsPage() {
<Card className="glass-panel"> <Card className="glass-panel">
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="flex flex-wrap items-center justify-between gap-4"> <SearchFilterBar
<div className="flex flex-wrap items-center gap-4 flex-1"> primary={
<div className="relative w-full max-w-md"> <div className="relative w-full max-w-md">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input <Input
@@ -249,126 +250,128 @@ function ClientConsentsPage() {
onChange={(e) => setSubjectInput(e.target.value)} onChange={(e) => setSubjectInput(e.target.value)}
/> />
</div> </div>
</div> }
<div className="flex items-center gap-3"> actions={
<Button <>
variant="ghost"
className={cn(
"gap-1 text-muted-foreground",
isAdvancedFilterOpen && "text-primary bg-primary/10",
)}
onClick={() => setIsAdvancedFilterOpen(!isAdvancedFilterOpen)}
>
<Filter className="h-4 w-4" />
{t(
"ui.dev.clients.consents.filters.advanced",
"Advanced Filters",
)}
</Button>
<Button
className="shadow-sm shadow-primary/30"
onClick={() => setSubject(subjectInput.trim())}
>
{t("ui.common.search", "검색")}
</Button>
<Button
className="shadow-sm shadow-primary/30"
onClick={handleExportCSV}
disabled={filteredRows.length === 0}
>
<Download className="h-4 w-4" />
{t("ui.dev.clients.consents.export_csv", "Export CSV")}
</Button>
</div>
</div>
{isAdvancedFilterOpen && (
<div className="flex flex-col gap-4 rounded-lg bg-secondary/30 p-4 border border-border/40 animate-in fade-in slide-in-from-top-2 duration-200">
<div className="flex flex-col gap-2">
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
{t("ui.dev.clients.consents.status_label", "Status:")}
</span>
<div className="flex flex-wrap gap-4">
<label className="flex items-center gap-2 text-sm cursor-pointer hover:text-foreground">
<input
type="checkbox"
className="rounded border-input text-primary focus:ring-primary h-4 w-4"
checked={statusFilter.includes("active")}
onChange={(e) =>
handleStatusFilterChange("active", e.target.checked)
}
/>
{t("ui.common.status.active", "Active")}
</label>
<label className="flex items-center gap-2 text-sm cursor-pointer hover:text-foreground">
<input
type="checkbox"
className="rounded border-input text-primary focus:ring-primary h-4 w-4"
checked={statusFilter.includes("revoked")}
onChange={(e) =>
handleStatusFilterChange("revoked", e.target.checked)
}
/>
{t("ui.dev.clients.consents.status_revoked", "Revoked")}
</label>
</div>
</div>
<div className="flex flex-col gap-2">
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
{t("ui.dev.clients.consents.scope_label", "Scope:")}
</span>
<div className="flex flex-wrap gap-4">
{allScopes.length > 0 && (
<label className="flex items-center gap-2 text-sm cursor-pointer font-bold text-primary hover:opacity-80">
<input
type="checkbox"
className="rounded border-input text-primary focus:ring-primary h-4 w-4"
checked={
scopeFilter.length === allScopes.length &&
allScopes.length > 0
}
onChange={(e) =>
handleAllScopesChange(e.target.checked)
}
/>
ALL
</label>
)}
{allScopes.map((scope) => (
<label
key={scope}
className="flex items-center gap-2 text-sm cursor-pointer hover:text-foreground"
>
<input
type="checkbox"
className="rounded border-input text-primary focus:ring-primary h-4 w-4"
checked={scopeFilter.includes(scope)}
onChange={(e) =>
handleScopeFilterChange(scope, e.target.checked)
}
/>
{scope}
</label>
))}
</div>
</div>
<div className="flex justify-end">
<Button <Button
variant="ghost" variant="ghost"
size="sm" className={cn(
className="text-xs text-muted-foreground p-0 h-auto" "gap-1 text-muted-foreground",
onClick={() => { isAdvancedFilterOpen && "text-primary bg-primary/10",
setStatusFilter([]); )}
setScopeFilter([]); onClick={() => setIsAdvancedFilterOpen(!isAdvancedFilterOpen)}
}}
> >
{t("ui.common.reset", "초기화")} <Filter className="h-4 w-4" />
{t(
"ui.dev.clients.consents.filters.advanced",
"Advanced Filters",
)}
</Button> </Button>
</div> <Button
</div> className="shadow-sm shadow-primary/30"
)} onClick={() => setSubject(subjectInput.trim())}
>
{t("ui.common.search", "검색")}
</Button>
<Button
className="shadow-sm shadow-primary/30"
onClick={handleExportCSV}
disabled={filteredRows.length === 0}
>
<Download className="h-4 w-4" />
{t("ui.dev.clients.consents.export_csv", "Export CSV")}
</Button>
</>
}
advancedOpen={isAdvancedFilterOpen}
advanced={
<>
<div className="flex flex-col gap-2">
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
{t("ui.dev.clients.consents.status_label", "Status:")}
</span>
<div className="flex flex-wrap gap-4">
<label className="flex items-center gap-2 text-sm cursor-pointer hover:text-foreground">
<input
type="checkbox"
className="rounded border-input text-primary focus:ring-primary h-4 w-4"
checked={statusFilter.includes("active")}
onChange={(e) =>
handleStatusFilterChange("active", e.target.checked)
}
/>
{t("ui.common.status.active", "Active")}
</label>
<label className="flex items-center gap-2 text-sm cursor-pointer hover:text-foreground">
<input
type="checkbox"
className="rounded border-input text-primary focus:ring-primary h-4 w-4"
checked={statusFilter.includes("revoked")}
onChange={(e) =>
handleStatusFilterChange("revoked", e.target.checked)
}
/>
{t("ui.dev.clients.consents.status_revoked", "Revoked")}
</label>
</div>
</div>
<div className="flex flex-col gap-2">
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
{t("ui.dev.clients.consents.scope_label", "Scope:")}
</span>
<div className="flex flex-wrap gap-4">
{allScopes.length > 0 && (
<label className="flex items-center gap-2 text-sm cursor-pointer font-bold text-primary hover:opacity-80">
<input
type="checkbox"
className="rounded border-input text-primary focus:ring-primary h-4 w-4"
checked={
scopeFilter.length === allScopes.length &&
allScopes.length > 0
}
onChange={(e) =>
handleAllScopesChange(e.target.checked)
}
/>
ALL
</label>
)}
{allScopes.map((scope) => (
<label
key={scope}
className="flex items-center gap-2 text-sm cursor-pointer hover:text-foreground"
>
<input
type="checkbox"
className="rounded border-input text-primary focus:ring-primary h-4 w-4"
checked={scopeFilter.includes(scope)}
onChange={(e) =>
handleScopeFilterChange(scope, e.target.checked)
}
/>
{scope}
</label>
))}
</div>
</div>
<div className="flex justify-end">
<Button
variant="ghost"
size="sm"
className="text-xs text-muted-foreground p-0 h-auto"
onClick={() => {
setStatusFilter([]);
setScopeFilter([]);
}}
>
{t("ui.common.reset", "초기화")}
</Button>
</div>
</>
}
/>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -36,6 +36,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "../../components/ui/table"; } from "../../components/ui/table";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { fetchClients, fetchDevStats } from "../../lib/devApi"; import { fetchClients, fetchDevStats } from "../../lib/devApi";
import { t } from "../../lib/i18n"; import { t } from "../../lib/i18n";
import { cn } from "../../lib/utils"; import { cn } from "../../lib/utils";
@@ -179,8 +180,9 @@ function ClientsPage() {
</Button> </Button>
</div> </div>
</div> </div>
<div className="mt-4 flex flex-col gap-3"> <SearchFilterBar
<div className="flex flex-col gap-3 md:flex-row md:items-center"> className="mt-4"
primary={
<div className="relative flex-1"> <div className="relative flex-1">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input <Input
@@ -193,7 +195,9 @@ function ClientsPage() {
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
/> />
</div> </div>
<div className="flex items-center gap-2"> }
actions={
<>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
@@ -220,65 +224,67 @@ function ClientsPage() {
{t("ui.dev.clients.badge.admin_session", "관리자 세션")} {t("ui.dev.clients.badge.admin_session", "관리자 세션")}
</Badge> </Badge>
</div> </div>
</div> </>
</div> }
advancedOpen={isAdvancedFilterOpen}
{isAdvancedFilterOpen && ( advanced={
<div className="flex flex-wrap items-center gap-6 rounded-lg bg-secondary/30 p-4 border border-border/40 animate-in fade-in slide-in-from-top-2 duration-200"> <>
<div className="flex items-center gap-2"> <div className="flex flex-wrap items-center gap-6">
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground whitespace-nowrap"> <div className="flex items-center gap-2">
{t("ui.dev.clients.filter.type_label", "Type:")} <span className="text-xs font-bold uppercase tracking-wider text-muted-foreground whitespace-nowrap">
</span> {t("ui.dev.clients.filter.type_label", "Type:")}
<select </span>
className="h-9 rounded-md border border-input bg-background px-3 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30 min-w-[140px]" <select
value={typeFilter} className="h-9 min-w-[140px] rounded-md border border-input bg-background px-3 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30"
onChange={(e) => setTypeFilter(e.target.value)} value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
>
<option value="all">
{t("ui.dev.clients.filter.type_all", "모든 유형")}
</option>
<option value="private">
{t("ui.dev.clients.type.private", "Server side App")}
</option>
<option value="pkce">
{t("ui.dev.clients.type.pkce", "PKCE")}
</option>
</select>
</div>
<div className="flex items-center gap-2">
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground whitespace-nowrap">
{t("ui.dev.clients.consents.status_label", "Status:")}
</span>
<select
className="h-9 min-w-[140px] rounded-md border border-input bg-background px-3 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
>
<option value="all">
{t("ui.dev.clients.filter.status_all", "모든 상태")}
</option>
<option value="active">
{t("ui.common.status.active", "Active")}
</option>
<option value="inactive">
{t("ui.common.status.inactive", "Inactive")}
</option>
</select>
</div>
<Button
variant="ghost"
size="sm"
className="ml-auto text-xs text-muted-foreground"
onClick={() => {
setTypeFilter("all");
setStatusFilter("all");
}}
> >
<option value="all"> {t("ui.common.reset", "초기화")}
{t("ui.dev.clients.filter.type_all", "모든 유형")} </Button>
</option>
<option value="private">
{t("ui.dev.clients.type.private", "Server side App")}
</option>
<option value="pkce">
{t("ui.dev.clients.type.pkce", "PKCE")}
</option>
</select>
</div> </div>
<div className="flex items-center gap-2"> </>
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground whitespace-nowrap"> }
{t("ui.dev.clients.consents.status_label", "Status:")} />
</span>
<select
className="h-9 rounded-md border border-input bg-background px-3 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30 min-w-[140px]"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
>
<option value="all">
{t("ui.dev.clients.filter.status_all", "모든 상태")}
</option>
<option value="active">
{t("ui.common.status.active", "Active")}
</option>
<option value="inactive">
{t("ui.common.status.inactive", "Inactive")}
</option>
</select>
</div>
<Button
variant="ghost"
size="sm"
className="text-xs text-muted-foreground ml-auto"
onClick={() => {
setTypeFilter("all");
setStatusFilter("all");
}}
>
{t("ui.common.reset", "초기화")}
</Button>
</div>
)}
</div>
</CardHeader> </CardHeader>
<CardContent className="pt-0"> <CardContent className="pt-0">
<div className="grid gap-4 md:grid-cols-3"> <div className="grid gap-4 md:grid-cols-3">