1
0
forked from baron/baron-sso

org chart 연동기능 추가

This commit is contained in:
2026-04-29 21:00:51 +09:00
parent 438f844f2b
commit 01e7b15c46
25 changed files with 5141 additions and 2940 deletions

View File

@@ -79,14 +79,73 @@ test.describe("User Management", () => {
],
},
},
{
id: "03dbe16b-e47b-4f72-927b-782807d67a35",
slug: "tech-planning",
name: "기술기획",
type: "USER_GROUP",
parentId: "hanmac-company-id",
config: {},
},
{
id: "hanmac-family-id",
slug: "hanmac-family",
name: "한맥가족",
type: "COMPANY_GROUP",
config: {},
},
{
id: "hanmac-company-id",
slug: "hanmac-company",
name: "한맥기술",
type: "COMPANY",
parentId: "hanmac-family-id",
config: {},
},
{
id: "hanmac-team-id",
slug: "hanmac-team",
name: "한맥팀",
type: "USER_GROUP",
parentId: "hanmac-company-id",
config: {},
},
{
id: "system-id",
slug: "system",
name: "System Tenant",
type: "SYSTEM",
config: {},
},
{
id: "external-tenant-id",
slug: "external-tenant",
name: "External Tenant",
type: "COMPANY",
config: {},
},
],
total: 1,
total: 7,
limit: 100,
offset: 0,
},
});
}
if (url.match(/\/admin\/tenants$/) && method === "POST") {
const postData = route.request().postDataJSON();
return route.fulfill({
status: 201,
json: {
id: "personal-tenant-id",
slug: postData?.slug || "personal",
name: postData?.name || "Personal",
type: postData?.type || "PERSONAL",
config: {},
},
});
}
if (url.match(/\/admin\/tenants\/t-1$/) && method === "GET") {
return route.fulfill({
json: {
@@ -107,6 +166,21 @@ test.describe("User Management", () => {
});
}
if (
url.match(/\/admin\/tenants\/03dbe16b-e47b-4f72-927b-782807d67a35$/) &&
method === "GET"
) {
return route.fulfill({
json: {
id: "03dbe16b-e47b-4f72-927b-782807d67a35",
slug: "tech-planning",
name: "기술기획",
type: "USER_GROUP",
config: {},
},
});
}
if (url.match(/\/admin\/users\/u-1$/) && method === "GET") {
return route.fulfill({
json: {
@@ -185,6 +259,7 @@ test.describe("User Management", () => {
name: "John Doe Updated",
email: "john@test.com",
loginId: "johndoe_updated",
status: "inactive",
},
});
}
@@ -197,9 +272,11 @@ test.describe("User Management", () => {
id: "u-1",
name: "John Doe",
email: "john@test.com",
phone: "010-1111-2222",
loginId: "johndoe",
role: "user",
status: "active",
createdAt: "2026-04-01T00:00:00Z",
},
],
total: 1,
@@ -285,8 +362,26 @@ test.describe("User Management", () => {
// Ensure the page title is loaded
await expect(page.getByText(/사용자 추가/i).first()).toBeVisible();
const userTypeTabs = page.getByRole("tab");
await expect(userTypeTabs).toHaveText([
"한맥가족 구성원",
"외부 기업 회원",
"개인 회원",
]);
await expect(page.getByRole("tab", { name: /외부 기업 회원/i })).toBeVisible();
await expect(
page.getByRole("tab", { name: /한맥가족 구성원/i }),
).toBeVisible();
await expect(page.getByRole("tab", { name: /개인 회원/i })).toBeVisible();
await expect(
page.getByRole("tab", { name: /한맥가족 구성원/i }),
).toHaveAttribute("data-state", "active");
await expect(
page.getByLabel(/한맥 가족 구성원으로 등록/i),
).toHaveCount(0);
// Select Tenant first (important for schema fields to show up)
await page.getByRole("tab", { name: /외부 기업 회원/i }).click();
await page.selectOption("select#tenantSlug", "test-tenant");
// Fill required fields
@@ -305,6 +400,303 @@ test.describe("User Management", () => {
await expect(page).toHaveURL(/.*\/users$/, { timeout: 10000 });
});
test("should export users through the authenticated API client", async ({
page,
}) => {
let authorizationHeader: string | undefined;
await page.route(/\/admin\/users\/export(\?.*)?$/, async (route) => {
authorizationHeader = route.request().headers().authorization;
return route.fulfill({
status: 200,
headers: {
"content-type": "text/csv; charset=utf-8",
"content-disposition": 'attachment; filename="users.csv"',
},
body: "email,name\njohn@test.com,John Doe\n",
});
});
await page.goto("/users");
const [download] = await Promise.all([
page.waitForEvent("download"),
page.getByRole("button", { name: /내보내기|Export/i }).click(),
]);
expect(download.suggestedFilename()).toBe("users.csv");
expect(authorizationHeader).toBe("Bearer fake-token");
});
test("should show contact info in one row, hide roles, and toggle user status", async ({
page,
}) => {
let updatePayload: Record<string, unknown> | undefined;
await page.route(/\/admin\/users\/u-1$/, async (route) => {
if (route.request().method() === "PUT") {
updatePayload = route.request().postDataJSON();
return route.fulfill({
json: {
id: "u-1",
name: "John Doe",
email: "john@test.com",
phone: "010-1111-2222",
loginId: "johndoe",
status: "inactive",
createdAt: "2026-04-01T00:00:00Z",
},
});
}
return route.fallback();
});
await page.goto("/users");
const table = page.locator("table");
await expect(
table.getByRole("columnheader", { name: /ROLE|역할/i }),
).toHaveCount(0);
await expect(page.getByTestId("user-contact-u-1")).toContainText(
"John Doe john@test.com 010-1111-2222",
);
await page.getByTestId("user-status-toggle-u-1").click();
await expect
.poll(() => updatePayload)
.toMatchObject({ status: "inactive" });
});
test("should create a Hanmac family user with tenant appointments and no representative affiliation", async ({
page,
}) => {
let createPayload: Record<string, unknown> | undefined;
await page.route(/\/admin\/users(\?.*)?$/, async (route) => {
if (route.request().method() === "POST") {
createPayload = route.request().postDataJSON();
return route.fulfill({
status: 201,
json: {
id: "new-user-id",
name: "Family User",
email: "family@test.com",
},
});
}
return route.fallback();
});
await page.goto("/users/new");
await expect(
page.getByRole("tab", { name: /한맥가족 구성원/i }),
).toHaveAttribute("data-state", "active");
await expect(
page.getByLabel(/한맥 가족 구성원으로 등록/i),
).toHaveCount(0);
await expect(page.locator("select#role")).toHaveCount(0);
await expect(page.locator("input#department")).toHaveCount(0);
await expect(page.getByText(/대표 소속/i)).toHaveCount(0);
await page.getByRole("button", { name: /^추가$/i }).click();
await expect(page.getByTestId("appointment-row-0")).toBeVisible();
await expect(
page.getByTestId("appointment-tenant-owner-line-0"),
).toBeVisible();
await expect(page.getByTestId("appointment-position-line-0")).toBeVisible();
await page.getByRole("button", { name: /테넌트 선택/i }).click();
await expect(page.getByTitle(/테넌트 선택/i)).toHaveAttribute(
"src",
/\/login\?auto=1&returnTo=%2Fembed%2Fpicker%3Fmode%3Dsingle%26select%3Dtenant%26width%3D400%26height%3D600%26tenantId%3Dhanmac-family-id$/,
);
await page.evaluate(() => {
window.dispatchEvent(
new MessageEvent("message", {
data: {
type: "orgfront:picker:confirm",
payload: {
mode: "single",
selections: [
{
type: "tenant",
id: "03dbe16b-e47b-4f72-927b-782807d67a35",
name: "기술기획",
},
],
},
},
}),
);
});
await expect(page.getByText("기술기획")).toBeVisible();
await page.getByLabel(/조직장/i).check();
await page.getByLabel(/^직무$/i).fill("플랫폼 운영");
await page.getByLabel(/^직급$/i).fill("책임");
await page.locator('input[name="name"]').fill("Family User");
await page.locator('input[name="email"]').fill("family@test.com");
await page.getByRole("button", { name: /생성/i }).click();
await expect
.poll(() => createPayload)
.toMatchObject({
metadata: {
hanmacFamily: true,
additionalAppointments: [
{
tenantId: "03dbe16b-e47b-4f72-927b-782807d67a35",
tenantSlug: "tech-planning",
tenantName: "기술기획",
isOwner: true,
jobTitle: "플랫폼 운영",
position: "책임",
},
],
},
});
expect(createPayload).not.toHaveProperty("role");
expect(createPayload).not.toHaveProperty("department");
expect(createPayload).not.toHaveProperty("tenantSlug");
expect(createPayload).not.toHaveProperty("companyCode");
expect(createPayload).not.toHaveProperty("primaryTenantId");
});
test("should hide Hanmac family subtree and system tenants when creating a non-family user", async ({
page,
}) => {
await page.goto("/users/new");
await page.getByRole("tab", { name: /외부 기업 회원/i }).click();
const tenantOptionValues = await page
.locator("select#tenantSlug option")
.evaluateAll((options) =>
options.map((option) => (option as HTMLOptionElement).value),
);
expect(tenantOptionValues).toContain("test-tenant");
expect(tenantOptionValues).toContain("external-tenant");
expect(tenantOptionValues).not.toContain("");
expect(tenantOptionValues).not.toContain("system");
expect(tenantOptionValues).not.toContain("hanmac-family");
expect(tenantOptionValues).not.toContain("hanmac-company");
expect(tenantOptionValues).not.toContain("hanmac-team");
expect(tenantOptionValues).not.toContain("tech-planning");
});
test("should create a personal user and provision Personal tenant when missing", async ({
page,
}) => {
let tenantPayload: Record<string, unknown> | undefined;
let createPayload: Record<string, unknown> | undefined;
await page.route(/\/admin\/tenants$/, async (route) => {
if (route.request().method() === "POST") {
tenantPayload = route.request().postDataJSON();
return route.fulfill({
status: 201,
json: {
id: "personal-tenant-id",
slug: "personal",
name: "Personal",
type: "PERSONAL",
config: {},
},
});
}
return route.fallback();
});
await page.route(/\/admin\/users(\?.*)?$/, async (route) => {
if (route.request().method() === "POST") {
createPayload = route.request().postDataJSON();
return route.fulfill({
status: 201,
json: {
id: "personal-user-id",
name: "Personal User",
email: "personal@test.com",
},
});
}
return route.fallback();
});
await page.goto("/users/new");
await page.getByRole("tab", { name: /개인 회원/i }).click();
await expect(page.getByTestId("personal-tenant-summary")).toContainText(
/Personal/i,
);
await page.locator('input[name="name"]').fill("Personal User");
await page.locator('input[name="email"]').fill("personal@test.com");
await page.getByRole("button", { name: /생성/i }).click();
await expect
.poll(() => tenantPayload)
.toMatchObject({ name: "Personal", slug: "personal", type: "PERSONAL" });
await expect
.poll(() => createPayload)
.toMatchObject({
tenantSlug: "personal",
metadata: { userType: "personal", hanmacFamily: false },
});
});
test("should show Hanmac family appointments layout on user detail", async ({
page,
}) => {
await page.route(/\/admin\/users\/u-1$/, async (route) => {
if (route.request().method() === "GET") {
return route.fulfill({
json: {
id: "u-1",
name: "Family User",
email: "family@test.com",
phone: "010-1111-2222",
loginId: "familyuser",
role: "user",
status: "active",
createdAt: "2026-04-01T00:00:00Z",
updatedAt: "2026-04-01T00:00:00Z",
metadata: {
hanmacFamily: true,
additionalAppointments: [
{
tenantId: "03dbe16b-e47b-4f72-927b-782807d67a35",
tenantSlug: "tech-planning",
tenantName: "기술기획",
isOwner: true,
jobTitle: "플랫폼 운영",
position: "책임",
},
],
},
},
});
}
return route.fallback();
});
await page.goto("/users/u-1");
await expect(
page.getByRole("tab", { name: /한맥가족 구성원/i }),
).toHaveAttribute("data-state", "active");
await expect(
page.getByLabel(/한맥 가족 구성원으로 등록/i),
).toHaveCount(0);
await expect(page.getByTestId("detail-appointment-row-0")).toBeVisible();
await expect(
page.getByTestId("detail-appointment-tenant-owner-line-0"),
).toContainText(/기술기획|조직장/);
await expect(
page.getByTestId("detail-appointment-position-line-0"),
).toBeVisible();
});
test("should show conflict error when creating with an existing Login ID", async ({
page,
}) => {
@@ -312,6 +704,8 @@ test.describe("User Management", () => {
await expect(page.getByText(/사용자 추가/i).first()).toBeVisible();
await page.getByRole("tab", { name: /외부 기업 회원/i }).click();
// Select Tenant first (important for schema fields to show up)
await page.selectOption("select#tenantSlug", "test-tenant");