From f072d37362e070ae38f1086c6aaefa03ffb9875c Mon Sep 17 00:00:00 2001 From: chan Date: Thu, 19 Mar 2026 13:13:27 +0900 Subject: [PATCH] feat: apply sticky header and inner scroll pattern to all table pages --- .../src/features/api-keys/ApiKeyListPage.tsx | 179 +++--- .../routes/TenantAdminsAndOwnersTab.tsx | 508 +++++++++--------- .../tenants/routes/TenantGroupsPage.tsx | 259 ++++----- .../tenants/routes/TenantListPage.tsx | 234 ++++---- .../tenants/routes/TenantSubTenantsPage.tsx | 129 ++--- .../tenants/routes/TenantUsersPage.tsx | 128 ++--- .../routes/GlobalUserGroupListPage.tsx | 124 ++--- .../routes/TenantUserGroupsTab.tsx | 78 +-- .../routes/UserGroupDetailPage.tsx | 312 +++++------ 9 files changed, 1007 insertions(+), 944 deletions(-) diff --git a/adminfront/src/features/api-keys/ApiKeyListPage.tsx b/adminfront/src/features/api-keys/ApiKeyListPage.tsx index 9e67663e..aa45ade1 100644 --- a/adminfront/src/features/api-keys/ApiKeyListPage.tsx +++ b/adminfront/src/features/api-keys/ApiKeyListPage.tsx @@ -63,8 +63,8 @@ function ApiKeyListPage() { }; return ( -
-
+
+
@@ -103,8 +103,8 @@ function ApiKeyListPage() {
- - + +
{t("ui.admin.api_keys.list.registry.title", "API Key Registry")} @@ -119,95 +119,102 @@ function ApiKeyListPage() {
{t("ui.common.badge.system", "System")}
- + {(errorMsg || fallbackError) && (
{errorMsg ?? fallbackError}
)} - - - - - {t("ui.admin.api_keys.list.table.name", "NAME")} - - - {t("ui.admin.api_keys.list.table.client_id", "CLIENT ID")} - - - {t("ui.admin.api_keys.list.table.scopes", "SCOPES")} - - - {t("ui.admin.api_keys.list.table.last_used", "LAST USED")} - - - {t("ui.admin.api_keys.list.table.actions", "ACTIONS")} - - - - - {query.isLoading && ( - - - {t("msg.common.loading", "로딩 중...")} - - - )} - {!query.isLoading && items.length === 0 && ( - - - {t( - "msg.admin.api_keys.list.empty", - "등록된 API 키가 없습니다.", - )} - - - )} - {items.map((key) => ( - - -
- - {key.name} -
-
- - {key.client_id} - - -
- {key.scopes.map((scope) => ( - +
+
+ + + + {t("ui.admin.api_keys.list.table.name", "NAME")} + + + {t("ui.admin.api_keys.list.table.client_id", "CLIENT ID")} + + + {t("ui.admin.api_keys.list.table.scopes", "SCOPES")} + + + {t("ui.admin.api_keys.list.table.last_used", "LAST USED")} + + + {t("ui.admin.api_keys.list.table.actions", "ACTIONS")} + + + + + {query.isLoading && ( + + + {t("msg.common.loading", "로딩 중...")} + + + )} + {!query.isLoading && items.length === 0 && ( + + + {t( + "msg.admin.api_keys.list.empty", + "등록된 API 키가 없습니다.", + )} + + + )} + {items.map((key) => ( + + +
+ + {key.name} +
+
+ + {key.client_id} + + +
+ {key.scopes.map((scope) => ( + + {scope} + + ))} +
+
+ + {key.lastUsedAt + ? new Date(key.lastUsedAt).toLocaleString("ko-KR") + : t("ui.common.never", "Never")} + + + - -
- ))} -
-
+ + {t("ui.common.delete", "삭제")} + + + + ))} + + +
+
diff --git a/adminfront/src/features/tenants/routes/TenantAdminsAndOwnersTab.tsx b/adminfront/src/features/tenants/routes/TenantAdminsAndOwnersTab.tsx index 56112c98..3ac0f505 100644 --- a/adminfront/src/features/tenants/routes/TenantAdminsAndOwnersTab.tsx +++ b/adminfront/src/features/tenants/routes/TenantAdminsAndOwnersTab.tsx @@ -207,260 +207,266 @@ export function TenantAdminsAndOwnersTab() { ); return ( -
- {/* Owners Card */} - - -
- - - {t("ui.admin.tenants.owners.title", "테넌트 소유자")} - - - {t( - "msg.admin.tenants.owners.subtitle", - "이 테넌트의 최상위 권한을 가진 소유자(조직장) 목록입니다.", - )} - -
- -
- -
- - - - - {t("ui.admin.tenants.owners.table_name", "이름")} - - - {t("ui.admin.tenants.owners.table_email", "이메일")} - - - {t("ui.admin.tenants.owners.table_actions", "액션")} - - - - - {ownersQuery.isLoading ? ( - - -
- - - ) : currentOwners.length === 0 ? ( - - -
- -

- {t( - "msg.admin.tenants.owners.empty", - "등록된 소유자가 없습니다.", - )} -

-
-
-
- ) : ( - currentOwners.map((owner) => ( - - -
-
- {owner.name.charAt(0)} -
- {owner.name} -
-
- - {owner.email} - - - - -
- )) +
+
+ {/* Owners Card */} + + +
+ + + {t("ui.admin.tenants.owners.title", "테넌트 소유자")} + + + {t( + "msg.admin.tenants.owners.subtitle", + "이 테넌트의 최상위 권한을 가진 소유자(조직장) 목록입니다.", )} - -
-
-
-
+ +
+ + + +
+
+ + + + + {t("ui.admin.tenants.owners.table_name", "이름")} + + + {t("ui.admin.tenants.owners.table_email", "이메일")} + + + {t("ui.admin.tenants.owners.table_actions", "액션")} + + + + + {ownersQuery.isLoading ? ( + + +
+ + + ) : currentOwners.length === 0 ? ( + + +
+ +

+ {t( + "msg.admin.tenants.owners.empty", + "등록된 소유자가 없습니다.", + )} +

+
+
+
+ ) : ( + currentOwners.map((owner) => ( + + +
+
+ {owner.name.charAt(0)} +
+ {owner.name} +
+
+ + {owner.email} + + + + +
+ )) + )} + +
+
+
+
+ - {/* Admins Card */} - - -
- - - {t("ui.admin.tenants.admins.title", "테넌트 관리자")} - - - {t( - "msg.admin.tenants.admins.subtitle", - "이 테넌트의 자원을 관리할 수 있는 사용자 목록입니다.", - )} - -
- -
- -
- - - - - {t("ui.admin.tenants.admins.table_name", "이름")} - - - {t("ui.admin.tenants.admins.table_email", "이메일")} - - - {t("ui.admin.tenants.admins.table_actions", "액션")} - - - - - {adminsQuery.isLoading ? ( - - -
- - - ) : currentAdmins.length === 0 ? ( - - -
- -

- {t( - "msg.admin.tenants.admins.empty", - "등록된 관리자가 없습니다.", - )} -

-
-
-
- ) : ( - currentAdmins.map((admin) => ( - - -
-
- {admin.name.charAt(0)} -
- {admin.name} -
-
- - {admin.email} - - - - -
- )) + {/* Admins Card */} + + +
+ + + {t("ui.admin.tenants.admins.title", "테넌트 관리자")} + + + {t( + "msg.admin.tenants.admins.subtitle", + "이 테넌트의 자원을 관리할 수 있는 사용자 목록입니다.", )} - -
-
-
-
+ + + + + +
+
+ + + + + {t("ui.admin.tenants.admins.table_name", "이름")} + + + {t("ui.admin.tenants.admins.table_email", "이메일")} + + + {t("ui.admin.tenants.admins.table_actions", "액션")} + + + + + {adminsQuery.isLoading ? ( + + +
+ + + ) : currentAdmins.length === 0 ? ( + + +
+ +

+ {t( + "msg.admin.tenants.admins.empty", + "등록된 관리자가 없습니다.", + )} +

+
+
+
+ ) : ( + currentAdmins.map((admin) => ( + + +
+
+ {admin.name.charAt(0)} +
+ {admin.name} +
+
+ + {admin.email} + + + + +
+ )) + )} + +
+
+
+
+ + {/* Common Dialog for adding users */} g.id === selectedGroupId); return ( -
-
+
+
{/* 그룹 생성 폼 */} - - + + {" "} {t("ui.admin.groups.create.title", "새 그룹 생성")} @@ -359,7 +359,7 @@ function TenantGroupsPage() { )} - +
{/* 멤버 관리 섹션 (선택된 그룹이 있을 때) */} {currentGroup && ( - - + + {t("msg.admin.groups.members.title", "[{{name}}] 멤버 관리", { @@ -541,8 +545,8 @@ function TenantGroupsPage() { )} - -
+ +
- - - - - {t("ui.admin.groups.members.table.name", "이름")} - - - {t("ui.admin.groups.members.table.email", "이메일")} - - - {t("ui.admin.groups.members.table.remove", "제거")} - - - - - {currentGroup.members?.length === 0 && ( - - - {t("msg.admin.groups.members.empty", "멤버가 없습니다.")} - - - )} - {currentGroup.members?.map((user) => ( - - {user.name} - - {user.email} - - - - - - ))} - -
+
+
+ + + + + {t("ui.admin.groups.members.table.name", "이름")} + + + {t("ui.admin.groups.members.table.email", "이메일")} + + + {t("ui.admin.groups.members.table.remove", "제거")} + + + + + {currentGroup.members?.length === 0 && ( + + + {t( + "msg.admin.groups.members.empty", + "멤버가 없습니다.", + )} + + + )} + {currentGroup.members?.map((user) => ( + + + {user.name} + + + {user.email} + + + + + + ))} + +
+
+
)} diff --git a/adminfront/src/features/tenants/routes/TenantListPage.tsx b/adminfront/src/features/tenants/routes/TenantListPage.tsx index d1aa14e6..a434ee14 100644 --- a/adminfront/src/features/tenants/routes/TenantListPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantListPage.tsx @@ -116,8 +116,8 @@ function TenantListPage() { }; return ( -
-
+
+
{t("ui.admin.tenants.breadcrumb.section", "Tenants")} @@ -156,8 +156,8 @@ function TenantListPage() {
- - + +
{t("ui.admin.tenants.registry.title", "Tenant Registry")} @@ -172,120 +172,132 @@ function TenantListPage() { {t("ui.common.badge.admin_only", "Admin only")} - + {(errorMsg || fallbackError) && (
{errorMsg ?? fallbackError}
)} - - - - - {t("ui.admin.tenants.table.name", "NAME")} - - - {t("ui.admin.tenants.table.type", "TYPE")} - - - {t("ui.admin.tenants.table.slug", "SLUG")} - - - {t("ui.admin.tenants.table.status", "STATUS")} - - - {t("ui.admin.tenants.table.members", "MEMBERS")} - - - {t("ui.admin.tenants.table.updated", "UPDATED")} - - - {t("ui.admin.tenants.table.actions", "ACTIONS")} - - - - - {query.isLoading && ( - - - {t("msg.common.loading", "로딩 중...")} - - - )} - {!query.isLoading && tenants.length === 0 && ( - - - {t( - "msg.admin.tenants.empty", - "아직 등록된 테넌트가 없습니다.", - )} - - - )} - {tenants.map((tenant) => ( - - {tenant.name} - - - {t( - `domain.tenant_type.${tenant.type?.toLowerCase()}`, - tenant.type, - )} - - - - {tenant.slug} - - - - {t(`ui.common.status.${tenant.status}`, tenant.status)} - - - - {tenant.memberCount} - - - {tenant.updatedAt - ? new Date(tenant.updatedAt).toLocaleString("ko-KR") - : "-"} - - -
-
+ + + + {t("ui.admin.tenants.table.name", "NAME")} + + + {t("ui.admin.tenants.table.type", "TYPE")} + + + {t("ui.admin.tenants.table.slug", "SLUG")} + + + {t("ui.admin.tenants.table.status", "STATUS")} + + + {t("ui.admin.tenants.table.members", "MEMBERS")} + + + {t("ui.admin.tenants.table.updated", "UPDATED")} + + + {t("ui.admin.tenants.table.actions", "ACTIONS")} + + + + + {query.isLoading && ( + + + {t("msg.common.loading", "로딩 중...")} + + + )} + {!query.isLoading && tenants.length === 0 && ( + + - - {t("ui.common.edit", "편집")} - - - - - - ))} - -
+ {t( + "msg.admin.tenants.empty", + "아직 등록된 테넌트가 없습니다.", + )} + + + )} + {tenants.map((tenant) => ( + + + {tenant.name} + + + + {t( + `domain.tenant_type.${tenant.type?.toLowerCase()}`, + tenant.type, + )} + + + + {tenant.slug} + + + + {t( + `ui.common.status.${tenant.status}`, + tenant.status, + )} + + + + {tenant.memberCount} + + + {tenant.updatedAt + ? new Date(tenant.updatedAt).toLocaleString("ko-KR") + : "-"} + + +
+ + +
+
+
+ ))} + + +
+
diff --git a/adminfront/src/features/tenants/routes/TenantSubTenantsPage.tsx b/adminfront/src/features/tenants/routes/TenantSubTenantsPage.tsx index 73276498..cd58c513 100644 --- a/adminfront/src/features/tenants/routes/TenantSubTenantsPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantSubTenantsPage.tsx @@ -34,8 +34,8 @@ function TenantSubTenantsPage() { const subTenants = data?.items ?? []; return ( - - + +
@@ -57,64 +57,73 @@ function TenantSubTenantsPage() { - - - - - - {t("ui.admin.tenants.sub.table.name", "NAME")} - - - {t("ui.admin.tenants.sub.table.slug", "SLUG")} - - - {t("ui.admin.tenants.sub.table.status", "STATUS")} - - - {t("ui.admin.tenants.sub.table.action", "ACTION")} - - - - - {subTenants.length === 0 && ( - - - {t("msg.admin.tenants.sub.empty", "하위 테넌트가 없습니다.")} - - - )} - {subTenants.map((tenant) => ( - - {tenant.name} - - {tenant.slug} - - - - {t(`ui.common.status.${tenant.status}`, tenant.status)} - - - - - - - ))} - -
+ +
+
+ + + + + {t("ui.admin.tenants.sub.table.name", "NAME")} + + + {t("ui.admin.tenants.sub.table.slug", "SLUG")} + + + {t("ui.admin.tenants.sub.table.status", "STATUS")} + + + {t("ui.admin.tenants.sub.table.action", "ACTION")} + + + + + {subTenants.length === 0 && ( + + + {t( + "msg.admin.tenants.sub.empty", + "하위 테넌트가 없습니다.", + )} + + + )} + {subTenants.map((tenant) => ( + + + {tenant.name} + + + {tenant.slug} + + + + {t(`ui.common.status.${tenant.status}`, tenant.status)} + + + + + + + ))} + +
+
+
); diff --git a/adminfront/src/features/tenants/routes/TenantUsersPage.tsx b/adminfront/src/features/tenants/routes/TenantUsersPage.tsx index fe722db9..e7f63104 100644 --- a/adminfront/src/features/tenants/routes/TenantUsersPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantUsersPage.tsx @@ -42,8 +42,8 @@ function TenantUsersPage() { const users = usersQuery.data?.items ?? []; return ( - - + + {t("ui.admin.tenants.members.title", "Tenant Members ({{count}})", { @@ -51,66 +51,70 @@ function TenantUsersPage() { })} - - - - - - {t("ui.admin.tenants.members.table.name", "NAME")} - - - {t("ui.admin.tenants.members.table.email", "EMAIL")} - - - {t("ui.admin.tenants.members.table.role", "ROLE")} - - - {t("ui.admin.tenants.members.table.status", "STATUS")} - - - - - {users.length === 0 && ( - - - {t( - "msg.admin.tenants.members.empty", - "소속된 사용자가 없습니다.", - )} - - - )} - {users.map((user) => ( - - {user.name} - -
- - {user.email} -
-
- - - {t( - `ui.common.role.${user.role}`, - user.role.replace("_", " "), - )} - - - - - {t(`ui.common.status.${user.status}`, user.status)} - - -
- ))} -
-
+ +
+
+ + + + + {t("ui.admin.tenants.members.table.name", "NAME")} + + + {t("ui.admin.tenants.members.table.email", "EMAIL")} + + + {t("ui.admin.tenants.members.table.role", "ROLE")} + + + {t("ui.admin.tenants.members.table.status", "STATUS")} + + + + + {users.length === 0 && ( + + + {t( + "msg.admin.tenants.members.empty", + "소속된 사용자가 없습니다.", + )} + + + )} + {users.map((user) => ( + + {user.name} + +
+ + {user.email} +
+
+ + + {t( + `ui.common.role.${user.role}`, + user.role.replace("_", " "), + )} + + + + + {t(`ui.common.status.${user.status}`, user.status)} + + +
+ ))} +
+
+
+
); diff --git a/adminfront/src/features/user-groups/routes/GlobalUserGroupListPage.tsx b/adminfront/src/features/user-groups/routes/GlobalUserGroupListPage.tsx index c4c84dc4..074a5f0f 100644 --- a/adminfront/src/features/user-groups/routes/GlobalUserGroupListPage.tsx +++ b/adminfront/src/features/user-groups/routes/GlobalUserGroupListPage.tsx @@ -35,8 +35,8 @@ export default function GlobalUserGroupListPage() { return
Loading tenants and groups...
; return ( -
-
+
+

User Groups

@@ -46,7 +46,7 @@ export default function GlobalUserGroupListPage() {

-
+
{tenantList?.items.map((tenant) => ( ))} @@ -62,8 +62,8 @@ function TenantGroupCard({ tenant }: { tenant: TenantSummary }) { }); return ( - - + +
@@ -83,62 +83,66 @@ function TenantGroupCard({ tenant }: { tenant: TenantSummary }) { - - - - - 그룹명 - 설명 - 멤버 수 - 작업 - - - - {isLoading ? ( - - - Loading... - - - ) : groups?.length === 0 ? ( - - - 등록된 유저 그룹이 없습니다. - - - ) : ( - groups?.map((group) => ( - - -
- - - {group.name} - -
-
- {group.description || "-"} - {group.members?.length || 0} 명 - - - + +
+
+
+ + + 그룹명 + 설명 + 멤버 수 + 작업 - )) - )} - -
+ + + {isLoading ? ( + + + Loading... + + + ) : groups?.length === 0 ? ( + + + 등록된 유저 그룹이 없습니다. + + + ) : ( + groups?.map((group) => ( + + +
+ + + {group.name} + +
+
+ {group.description || "-"} + {group.members?.length || 0} 명 + + + +
+ )) + )} +
+ +
+
); diff --git a/adminfront/src/features/user-groups/routes/TenantUserGroupsTab.tsx b/adminfront/src/features/user-groups/routes/TenantUserGroupsTab.tsx index c56f8702..ad4ffdc7 100644 --- a/adminfront/src/features/user-groups/routes/TenantUserGroupsTab.tsx +++ b/adminfront/src/features/user-groups/routes/TenantUserGroupsTab.tsx @@ -929,9 +929,9 @@ function TenantUserGroupsTab() { const BaseIcon = getTenantIcon(currentBase.type); return ( -
- - +
+ +
@@ -1078,7 +1078,7 @@ function TenantUserGroupsTab() {
-
+
)}
- - - - - - {t("ui.admin.tenants.table.name", "NAME")} - - - {t("ui.admin.tenants.table.slug", "SLUG")} - - - {t("ui.admin.tenants.table.members", "MEMBERS")} - - - {t("ui.admin.tenants.table.status", "STATUS")} - - - {t("ui.admin.tenants.table.actions", "ACTIONS")} - - - - - - -
+ +
+
+ + + + + {t("ui.admin.tenants.table.name", "NAME")} + + + {t("ui.admin.tenants.table.slug", "SLUG")} + + + {t("ui.admin.tenants.table.members", "MEMBERS")} + + + {t("ui.admin.tenants.table.status", "STATUS")} + + + {t("ui.admin.tenants.table.actions", "ACTIONS")} + + + + + + +
+
+
diff --git a/adminfront/src/features/user-groups/routes/UserGroupDetailPage.tsx b/adminfront/src/features/user-groups/routes/UserGroupDetailPage.tsx index 98b9f9c7..860a8142 100644 --- a/adminfront/src/features/user-groups/routes/UserGroupDetailPage.tsx +++ b/adminfront/src/features/user-groups/routes/UserGroupDetailPage.tsx @@ -211,8 +211,8 @@ export function UserGroupDetailPage() { ); return ( -
-
+
+
-
+
{/* Members Management */} - - + +
{t("ui.admin.groups.detail.members_title", "구성원 관리")} @@ -347,88 +347,90 @@ export function UserGroupDetailPage() { - -
- - - - - {t("ui.admin.users.list.table.name_email", "사용자")} - - - {t("ui.admin.groups.table.actions", "액션")} - - - - - {!currentGroup.members || - currentGroup.members.length === 0 ? ( + +
+
+
+ - - {t( - "msg.admin.groups.members.empty", - "구성원이 없습니다.", - )} - + + {t("ui.admin.users.list.table.name_email", "사용자")} + + + {t("ui.admin.groups.table.actions", "액션")} + - ) : ( - currentGroup.members.map((member) => ( - - -
-
- {member.name.charAt(0)} -
-
-

- {member.name} -

-

- {member.email} -

-
-
-
- - +
+ + {!currentGroup.members || + currentGroup.members.length === 0 ? ( + + + {t( + "msg.admin.groups.members.empty", + "구성원이 없습니다.", + )} - )) - )} - -
+ ) : ( + currentGroup.members.map((member) => ( + + +
+
+ {member.name.charAt(0)} +
+
+

+ {member.name} +

+

+ {member.email} +

+
+
+
+ + + +
+ )) + )} + + +
{/* Roles/Permissions Management (Keto Based) */} - - + +
{t("ui.admin.groups.detail.permissions_title", "권한 관리")} @@ -530,86 +532,88 @@ export function UserGroupDetailPage() { - -
- - - - - {t("ui.admin.users.detail.form.tenant", "대상 테넌트")} - - - {t("ui.admin.users.detail.form.role", "역할")} - - - {t("ui.admin.groups.table.actions", "액션")} - - - - - {isRolesLoading ? ( + +
+
+
+ - -
- + + {t("ui.admin.users.detail.form.tenant", "대상 테넌트")} + + + {t("ui.admin.users.detail.form.role", "역할")} + + + {t("ui.admin.groups.table.actions", "액션")} + - ) : !groupRoles || groupRoles.length === 0 ? ( - - - {t( - "msg.admin.groups.roles.empty", - "할당된 역할이 없습니다.", - )} - - - ) : ( - groupRoles.map((role, idx) => ( - - -
- {role.tenantName || role.tenantId} -
-
- - - {role.relation} - - - - + + + {isRolesLoading ? ( + + +
- )) - )} - -
+ ) : !groupRoles || groupRoles.length === 0 ? ( + + + {t( + "msg.admin.groups.roles.empty", + "할당된 역할이 없습니다.", + )} + + + ) : ( + groupRoles.map((role, idx) => ( + + +
+ {role.tenantName || role.tenantId} +
+
+ + + {role.relation} + + + + + +
+ )) + )} + + +