1
0
forked from baron/baron-sso

devfront 관계 탭 사용자 검색·다중선택 UX 개선

This commit is contained in:
2026-04-15 18:23:23 +09:00
parent f955d23ef1
commit a79c350831
7 changed files with 519 additions and 60 deletions

View File

@@ -26,6 +26,14 @@ test.describe("DevFront relationships", () => {
const state = {
clients: [makeClient("client-rel", { name: "Relations app" })],
consents: [] as Consent[],
users: [
{
id: "user-2",
name: "홍길동",
email: "hong@example.com",
loginId: "hong01",
},
],
relations: {
"client-rel": [
{
@@ -33,6 +41,8 @@ test.describe("DevFront relationships", () => {
subject: "User:user-1",
subjectType: "User",
subjectId: "user-1",
userName: "기존 사용자",
userEmail: "existing@example.com",
},
] satisfies ClientRelation[],
},
@@ -48,14 +58,17 @@ test.describe("DevFront relationships", () => {
await expect(
page.getByRole("heading", { name: "부여된 관계" }),
).toBeVisible();
await expect(page.getByText("기존 사용자")).toBeVisible();
await expect(page.getByText("User:user-1")).toBeVisible();
await page.getByLabel(/^관계$/).selectOption("secret_rotator");
await page.getByLabel(/^사용자 ID$/).fill("user-2");
await page.getByLabel(/^사용자$/).fill("홍길동");
await page.getByRole("button", { name: /홍길동/ }).click();
await page.getByLabel(/시크릿 재발급/).check();
await page.getByLabel(/동의 조회/).check();
await page.getByRole("button", { name: /^추가$/ }).click();
await expect(page.getByText("User:user-2")).toBeVisible();
await expect.poll(() => state.relations["client-rel"]?.length ?? 0).toBe(2);
await expect.poll(() => state.relations["client-rel"]?.length ?? 0).toBe(3);
await page
.locator("tr")
@@ -63,7 +76,13 @@ test.describe("DevFront relationships", () => {
.getByRole("button", { name: /Delete|삭제/i })
.click();
await expect(page.getByText("User:user-2")).toHaveCount(0);
await expect.poll(() => state.relations["client-rel"]?.length ?? 0).toBe(1);
await expect
.poll(
() =>
state.relations["client-rel"]?.filter(
(item) => item.subject === "User:user-2",
).length ?? 0,
)
.toBe(1);
});
});

View File

@@ -58,6 +58,16 @@ export type ClientRelation = {
subject: string;
subjectType: string;
subjectId: string;
userName?: string;
userEmail?: string;
userLoginId?: string;
};
export type DevAssignableUser = {
id: string;
name: string;
email: string;
loginId?: string;
};
export type AuditLog = {
@@ -75,6 +85,7 @@ export type DevApiMockState = {
clients: Client[];
consents: Consent[];
relations?: Record<string, ClientRelation[]>;
users?: DevAssignableUser[];
auditLogsByCursor?: Record<
string,
{ items: AuditLog[]; next_cursor?: string }
@@ -261,6 +272,20 @@ export async function installDevApiMock(page: Page, state: DevApiMockState) {
});
}
if (pathname === "/api/v1/dev/users" && method === "GET") {
const search = (searchParams.get("search") || "").toLowerCase();
const limit = Number.parseInt(searchParams.get("limit") || "10", 10);
const items = (state.users ?? [])
.filter((user) => {
if (!search) return true;
return [user.name, user.email, user.loginId ?? ""].some((value) =>
value.toLowerCase().includes(search),
);
})
.slice(0, Number.isFinite(limit) ? limit : 10);
return json(route, { items });
}
if (pathname === "/api/v1/dev/clients" && method === "POST") {
const payload = (request.postDataJSON() as {
name?: string;