1
0
forked from baron/baron-sso

조직도 표현 개선

This commit is contained in:
2026-05-29 10:33:15 +09:00
parent 6a6730b544
commit c489c7c38f
34 changed files with 1872 additions and 391 deletions

View File

@@ -81,6 +81,7 @@ import {
} from "../../components/ui/table";
import { toast } from "../../components/ui/use-toast";
import {
type TenantSummary,
type UserSummary,
bulkDeleteUsers,
bulkUpdateUsers,
@@ -130,11 +131,115 @@ function assignableSystemRoleValue(role?: string | null) {
return isSuperAdminRole(role) ? "super_admin" : "user";
}
function userMatchesSearch(user: UserSummary, search: string) {
const normalizedSearch = search.trim().toLowerCase();
if (!normalizedSearch) {
return true;
}
return [
user.name,
user.email,
user.phone,
user.id,
user.tenantSlug,
user.tenant?.name,
user.department,
].some((value) => value?.toLowerCase().includes(normalizedSearch));
}
type UserListSearchControlsProps = {
search: string;
selectedCompany: string;
tenants: TenantSummary[];
profileRole?: string | null;
onSearch: (value: string) => void;
onCompanyChange: (value: string) => void;
};
const UserListSearchControls = React.memo(function UserListSearchControls({
search,
selectedCompany,
tenants,
profileRole,
onSearch,
onCompanyChange,
}: UserListSearchControlsProps) {
const [searchDraft, setSearchDraft] = React.useState(search);
React.useEffect(() => {
setSearchDraft(search);
}, [search]);
const handleSearch = React.useCallback(() => {
onSearch(searchDraft);
}, [onSearch, searchDraft]);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
handleSearch();
}
},
[handleSearch],
);
const tenantOptions = React.useMemo(
() =>
tenants.map((tenant) => (
<option key={tenant.id} value={tenant.slug}>
{tenant.name}
</option>
)),
[tenants],
);
return (
<SearchFilterBar
primary={
<>
<div className="relative w-48">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder={t(
"ui.admin.users.list.search_placeholder",
"이름 또는 이메일 검색...",
)}
className="h-9 pl-9"
value={searchDraft}
onChange={(event) => setSearchDraft(event.target.value)}
onKeyDown={handleKeyDown}
/>
</div>
<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"
value={selectedCompany}
onChange={(event) => onCompanyChange(event.target.value)}
disabled={profileRole === "tenant_admin"}
>
<option value="">{t("ui.common.all", "전체 테넌트")}</option>
{tenantOptions}
</select>
<Button
variant="secondary"
size="sm"
onClick={handleSearch}
className="h-9"
>
{t("ui.common.search", "검색")}
</Button>
</>
}
/>
);
});
function UserListPage() {
const navigate = useNavigate();
const [page, setPage] = React.useState(1);
const [search, setSearch] = React.useState("");
const [searchDraft, setSearchDraft] = React.useState("");
const [selectedCompany, setSelectedCompany] = React.useState<string>("");
const [visibleColumns, setVisibleColumns] = React.useState<
Record<string, boolean>
@@ -254,16 +359,15 @@ function UserListPage() {
},
});
const handleSearch = () => {
setSearch(searchDraft);
const handleSearch = React.useCallback((nextSearch: string) => {
setSearch(nextSearch);
setPage(1);
};
}, []);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
handleSearch();
}
};
const handleCompanyChange = React.useCallback((nextCompany: string) => {
setSelectedCompany(nextCompany);
setPage(1);
}, []);
const handleExport = (includeIds = false) => {
exportMutation.mutate(includeIds);
@@ -279,7 +383,14 @@ function UserListPage() {
)
: null;
const rawItems = query.data?.items ?? [];
const serverItems = query.data?.items ?? [];
const rawItems = React.useMemo(() => {
if (!query.isFetching || search.trim() === "") {
return serverItems;
}
return serverItems.filter((user) => userMatchesSearch(user, search));
}, [query.isFetching, search, serverItems]);
const userSortResolvers = React.useMemo<
SortResolverMap<UserSummary, UserSortKey>
>(
@@ -436,52 +547,13 @@ function UserListPage() {
)}
actions={
<>
<SearchFilterBar
primary={
<>
<div className="relative w-48">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder={t(
"ui.admin.users.list.search_placeholder",
"이름 또는 이메일 검색...",
)}
className="h-9 pl-9"
value={searchDraft}
onChange={(e) => setSearchDraft(e.target.value)}
onKeyDown={handleKeyDown}
/>
</div>
<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"
value={selectedCompany}
onChange={(e) => {
setSelectedCompany(e.target.value);
setPage(1);
}}
disabled={profile?.role === "tenant_admin"}
>
<option value="">
{t("ui.common.all", "전체 테넌트")}
</option>
{tenants.map((t) => (
<option key={t.id} value={t.slug}>
{t.name}
</option>
))}
</select>
<Button
variant="secondary"
size="sm"
onClick={handleSearch}
className="h-9"
>
{t("ui.common.search", "검색")}
</Button>
</>
}
<UserListSearchControls
search={search}
selectedCompany={selectedCompany}
tenants={tenants}
profileRole={profile?.role}
onSearch={handleSearch}
onCompanyChange={handleCompanyChange}
/>
<Button