1
0
forked from baron/baron-sso

조직도 M2M조회 추가, 자동로그인 보완

This commit is contained in:
2026-05-13 13:44:30 +09:00
parent 72288f1d39
commit 8c2b2f71ef
29 changed files with 2985 additions and 81 deletions

View File

@@ -72,6 +72,13 @@ const AVAILABLE_SCOPES = [
descKey: "msg.admin.api_keys.scopes.tenant_write.desc",
descFallback: "테넌트 정보를 직접 제어합니다.",
},
{
id: "org-context:read",
labelKey: "ui.admin.api_keys.scopes.org_context_read.title",
labelFallback: "조직 Context 조회",
descKey: "msg.admin.api_keys.scopes.org_context_read.desc",
descFallback: "외부 연동앱이 OrgFront SSOT 조직 JSON을 조회합니다.",
},
];
function ApiKeyCreatePage() {

View File

@@ -7,6 +7,7 @@ import {
ChevronDown,
ChevronRight,
CornerDownRight,
Download,
ExternalLink,
FolderOpen,
LayoutDashboard,
@@ -72,6 +73,7 @@ import {
type TenantSummary,
type UserSummary,
createUser,
exportTenantsCSV,
fetchTenants,
fetchUsers,
updateTenant,
@@ -422,6 +424,24 @@ function TenantUserGroupsTab() {
const [isAddExistingOpen, setIsAddExistingOpen] = useState(false);
const [existingSearch, setExistingSearch] = useState("");
const exportChildrenMutation = useMutation({
mutationFn: (parentId: string) => exportTenantsCSV(true, parentId),
onSuccess: ({ blob, filename }) => {
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
},
onError: () =>
toast.error(
t("msg.admin.tenants.export_error", "테넌트 내보내기에 실패했습니다."),
),
});
// Data Fetching
const {
data: allTenantsData,
@@ -611,6 +631,16 @@ function TenantUserGroupsTab() {
<UserPlus size={16} className="mr-2" />
{t("ui.admin.users.list.add", "멤버 추가")}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => exportChildrenMutation.mutate(selectedNode.id)}
disabled={exportChildrenMutation.isPending}
data-testid="tenant-subtree-export-btn"
>
<Download size={16} className="mr-2" />
{t("ui.admin.tenants.sub.export", "하위 조직 CSV")}
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>

View File

@@ -241,9 +241,9 @@ export async function deleteTenantsBulk(ids: string[]) {
});
}
export async function exportTenantsCSV(includeIds = false) {
export async function exportTenantsCSV(includeIds = false, parentId?: string) {
const response = await apiClient.get<Blob>("/v1/admin/tenants/export", {
params: { includeIds },
params: { includeIds, parentId: parentId || undefined },
responseType: "blob",
});
const dispositionHeader = response.headers["content-disposition"];

View File

@@ -616,6 +616,69 @@ test.describe("Tenants Management", () => {
).toBeVisible();
});
test("should export selected tenant children with UUIDs from organization tab", async ({
page,
}) => {
const parentId = "11111111-2222-4333-8444-555555555555";
const childId = "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee";
let exportUrl = "";
const mockTenants = [
{
id: parentId,
name: "Parent Org",
slug: "parent-org",
status: "active",
type: "COMPANY",
memberCount: 5,
parentId: null,
},
{
id: childId,
name: "Child Org",
slug: "child-org",
status: "active",
type: "ORGANIZATION",
memberCount: 2,
parentId,
},
];
await page.route("**/api/v1/admin/tenants**", async (route) => {
const url = route.request().url();
const headers = { "Access-Control-Allow-Origin": "*" };
if (url.includes("/export")) {
exportUrl = url;
return route.fulfill({
body: "tenant_id,name,type,parent_tenant_id,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type\n",
contentType: "text/csv",
headers: {
...headers,
"Content-Disposition": 'attachment; filename="tenants.csv"',
},
});
}
if (url.includes(`/admin/tenants/${parentId}`)) {
return route.fulfill({ json: mockTenants[0], headers });
}
return route.fulfill({
json: { items: mockTenants, total: 2, limit: 1000, offset: 0 },
headers,
});
});
await page.goto(`/tenants/${parentId}/organization`);
await expect(page.getByRole("heading", { name: "Child Org" })).toBeVisible({
timeout: 20000,
});
const download = page.waitForEvent("download");
await page.getByTestId("tenant-subtree-export-btn").click();
await download;
expect(exportUrl).toContain("includeIds=true");
expect(exportUrl).toContain(`parentId=${parentId}`);
});
test("should show tenant UUID at the top of tenant detail profile", async ({
page,
}) => {