forked from baron/baron-sso
orgfront 버그 픽스
This commit is contained in:
@@ -40,6 +40,15 @@ type ViewBox = {
|
||||
height: number;
|
||||
};
|
||||
|
||||
type OrgChartLoadErrorDiagnostics = {
|
||||
cacheMode: "redis" | "public";
|
||||
code: string;
|
||||
message: string;
|
||||
route: string;
|
||||
status: number | null;
|
||||
tenantId: string;
|
||||
};
|
||||
|
||||
type OrgSelectionDescendantOption = {
|
||||
depth: 1 | 2;
|
||||
id: string;
|
||||
@@ -1215,6 +1224,36 @@ function normalizeOrgSlug(value: unknown) {
|
||||
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
||||
}
|
||||
|
||||
function readErrorText(value: unknown): string {
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
function getOrgChartLoadErrorDiagnostics(
|
||||
error: unknown,
|
||||
options: { cacheMode: "redis" | "public"; route: string; tenantId: string },
|
||||
): OrgChartLoadErrorDiagnostics {
|
||||
const maybeError = error as {
|
||||
config?: { url?: string };
|
||||
message?: string;
|
||||
response?: {
|
||||
data?: { code?: unknown; error?: unknown; message?: unknown };
|
||||
status?: number;
|
||||
};
|
||||
};
|
||||
const responseData = maybeError.response?.data;
|
||||
return {
|
||||
cacheMode: options.cacheMode,
|
||||
code: readErrorText(responseData?.code),
|
||||
message:
|
||||
readErrorText(responseData?.error) ||
|
||||
readErrorText(responseData?.message) ||
|
||||
readErrorText(maybeError.message),
|
||||
route: maybeError.config?.url || options.route,
|
||||
status: maybeError.response?.status ?? null,
|
||||
tenantId: options.tenantId,
|
||||
};
|
||||
}
|
||||
|
||||
function getUserOrgAppointmentRefs(user: UserSummary): UserOrgAppointmentRef[] {
|
||||
const rawAppointments = user.metadata?.additionalAppointments;
|
||||
if (!Array.isArray(rawAppointments)) return [];
|
||||
@@ -1443,7 +1482,10 @@ export function TenantOrgChartPage() {
|
||||
}
|
||||
|
||||
const rootNodes = buildTenantFullTree(
|
||||
filterSystemGlobalTenants(orgChartSnapshotQuery.data.tenants, visibilityMode),
|
||||
filterSystemGlobalTenants(
|
||||
orgChartSnapshotQuery.data.tenants,
|
||||
visibilityMode,
|
||||
),
|
||||
).subTree;
|
||||
const membershipRootNodes = buildTenantFullTree(
|
||||
filterOrgChartMembershipTenants(orgChartSnapshotQuery.data.tenants),
|
||||
@@ -1601,6 +1643,24 @@ export function TenantOrgChartPage() {
|
||||
const isError = shareToken
|
||||
? publicQuery.isError
|
||||
: orgChartSnapshotQuery.isError;
|
||||
const currentLoadError = shareToken
|
||||
? publicQuery.error
|
||||
: orgChartSnapshotQuery.error;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentLoadError) return;
|
||||
console.error(
|
||||
"[orgfront] Org chart load failed",
|
||||
getOrgChartLoadErrorDiagnostics(currentLoadError, {
|
||||
cacheMode: shareToken ? "public" : "redis",
|
||||
route: shareToken
|
||||
? "/v1/public/orgchart"
|
||||
: "/v1/admin/orgchart/snapshot",
|
||||
tenantId:
|
||||
tenantId ?? window.localStorage.getItem("dev_tenant_id") ?? "",
|
||||
}),
|
||||
);
|
||||
}, [currentLoadError, shareToken, tenantId]);
|
||||
|
||||
const totalUsers = React.useMemo(() => {
|
||||
const ids = new Set<string>();
|
||||
@@ -1619,9 +1679,12 @@ export function TenantOrgChartPage() {
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
const errorMessage = shareToken
|
||||
? "조직도를 불러올 수 없거나 만료된 링크입니다."
|
||||
: "조직도를 불러올 수 없습니다. 로그인 상태와 조직 권한을 확인해 주세요.";
|
||||
return (
|
||||
<div className="p-8 text-center text-red-500">
|
||||
조직도를 불러올 수 없거나 만료된 링크입니다.
|
||||
{errorMessage}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
46
orgfront/src/lib/tenantTree.test.ts
Normal file
46
orgfront/src/lib/tenantTree.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { TenantSummary } from "./adminApi";
|
||||
import { buildTenantFullTree } from "./tenantTree";
|
||||
|
||||
function tenant(
|
||||
id: string,
|
||||
slug: string,
|
||||
parentId?: string,
|
||||
type = "USER_GROUP",
|
||||
): TenantSummary {
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
name: slug,
|
||||
slug,
|
||||
description: "",
|
||||
status: "active",
|
||||
parentId,
|
||||
memberCount: 0,
|
||||
totalMemberCount: 0,
|
||||
createdAt: "2026-06-10T00:00:00.000Z",
|
||||
updatedAt: "2026-06-10T00:00:00.000Z",
|
||||
};
|
||||
}
|
||||
|
||||
describe("buildTenantFullTree", () => {
|
||||
it("treats a self-parent hanmac-family tenant as a root", () => {
|
||||
const result = buildTenantFullTree([
|
||||
tenant(
|
||||
"hanmac-family-id",
|
||||
"hanmac-family",
|
||||
"hanmac-family-id",
|
||||
"COMPANY_GROUP",
|
||||
),
|
||||
tenant("saman-id", "saman", "hanmac-family-id", "COMPANY"),
|
||||
tenant("platform-id", "platform", "saman-id"),
|
||||
]);
|
||||
|
||||
expect(result.subTree).toHaveLength(1);
|
||||
expect(result.subTree[0]?.id).toBe("hanmac-family-id");
|
||||
expect(result.subTree[0]?.children[0]?.id).toBe("saman-id");
|
||||
expect(result.subTree[0]?.children[0]?.children[0]?.id).toBe(
|
||||
"platform-id",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -60,9 +60,9 @@ export function buildTenantFullTree(
|
||||
return total;
|
||||
};
|
||||
|
||||
// Calculate for all top-level nodes (those without parent)
|
||||
// Calculate for all top-level nodes (those without parent or with self-parent)
|
||||
for (const node of tenantMap.values()) {
|
||||
if (!node.parentId) {
|
||||
if (!node.parentId || node.parentId === node.id) {
|
||||
visitedForCalc.clear();
|
||||
calculateRecursive(node);
|
||||
}
|
||||
@@ -81,6 +81,8 @@ export function buildTenantFullTree(
|
||||
}
|
||||
|
||||
// If no rootId, return all top-level roots as subTree
|
||||
const roots = Array.from(tenantMap.values()).filter((n) => !n.parentId);
|
||||
const roots = Array.from(tenantMap.values()).filter(
|
||||
(n) => !n.parentId || n.parentId === n.id,
|
||||
);
|
||||
return { currentBase: null, subTree: roots };
|
||||
}
|
||||
|
||||
@@ -308,7 +308,9 @@ test("org chart defaults to hanmac family when public sector group is listed fir
|
||||
await page.goto("/chart");
|
||||
|
||||
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
|
||||
await expect(page.getByRole("button", { name: "조직: 한맥가족" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "조직: 한맥가족" }),
|
||||
).toBeVisible();
|
||||
await expect(svg.getByText("한맥가족", { exact: true })).toBeVisible();
|
||||
await expect(svg.getByText("삼안", { exact: true })).toBeVisible();
|
||||
await expect(svg.getByText("공공기관", { exact: true })).toHaveCount(0);
|
||||
|
||||
@@ -515,10 +515,13 @@ test("org chart allows a user in a hanmac-family descendant tenant", async ({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
tenants: [
|
||||
{
|
||||
...tenant("hanmac-family-id", "한맥가족", "hanmac-family"),
|
||||
type: "COMPANY_GROUP",
|
||||
},
|
||||
tenant(
|
||||
"hanmac-family-id",
|
||||
"한맥가족",
|
||||
"hanmac-family",
|
||||
"hanmac-family-id",
|
||||
"COMPANY_GROUP",
|
||||
),
|
||||
tenant("saman-id", "삼안", "saman", "hanmac-family-id", "COMPANY"),
|
||||
tenant("saman-platform-id", "플랫폼팀", "saman-platform", "saman-id"),
|
||||
],
|
||||
@@ -562,6 +565,57 @@ test("org chart allows a user in a hanmac-family descendant tenant", async ({
|
||||
await expect(svg.getByText(/Saman Descendant User/)).toBeVisible();
|
||||
});
|
||||
|
||||
test("org chart logs authenticated snapshot failures with actionable diagnostics", async ({
|
||||
page,
|
||||
}) => {
|
||||
const consoleMessages: string[] = [];
|
||||
page.on("console", async (message) => {
|
||||
if (message.type() !== "error") return;
|
||||
const values = await Promise.all(
|
||||
message.args().map((arg) => arg.jsonValue().catch(() => "")),
|
||||
);
|
||||
consoleMessages.push(JSON.stringify(values));
|
||||
});
|
||||
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("playwright_auth_bypass", "1");
|
||||
window.localStorage.setItem("dev_tenant_id", "saman-id");
|
||||
});
|
||||
|
||||
await page.route("**/api/v1/admin/orgchart/snapshot**", async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
status: 503,
|
||||
body: JSON.stringify({
|
||||
code: "service_unavailable",
|
||||
error: "tenant root traversal failed",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/chart");
|
||||
|
||||
await expect(
|
||||
page.getByText(
|
||||
"조직도를 불러올 수 없습니다. 로그인 상태와 조직 권한을 확인해 주세요.",
|
||||
),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("조직도를 불러올 수 없거나 만료된 링크입니다."),
|
||||
).toHaveCount(0);
|
||||
await expect
|
||||
.poll(() =>
|
||||
consoleMessages.some(
|
||||
(message) =>
|
||||
message.includes("[orgfront] Org chart load failed") &&
|
||||
message.includes("service_unavailable") &&
|
||||
message.includes("saman-id") &&
|
||||
message.includes("/v1/admin/orgchart/snapshot"),
|
||||
),
|
||||
)
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
test("org chart places GPDTDC representative users on visible leaf appointments", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user