1
0
forked from baron/baron-sso

orgfront 버그 픽스

This commit is contained in:
2026-06-10 09:36:57 +09:00
parent 28478309fa
commit c880b3c333
33 changed files with 853 additions and 130 deletions

View File

@@ -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>
);
}

View 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",
);
});
});

View File

@@ -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 };
}

View File

@@ -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);

View File

@@ -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,
}) => {