개선관리 요구사항과 Google 로그인을 완료

This commit is contained in:
Yun Chan 2026-08-28 16:07:09 +09:00
parent cc0a15b7c6
commit 2a39636163
112 changed files with 10166 additions and 527 deletions

View file

@ -30,6 +30,7 @@ interface AdminManagedUser {
email: string;
display_name: string;
role: "learner" | "teacher" | "admin";
learner_feedback_enabled: boolean;
account_status: "pending" | "approved" | "suspended";
affiliation: string;
cohort_ids: string[];
@ -676,7 +677,9 @@ async function expectCreateUserControlsFit(page: Page, viewportWidth: number) {
const clippedControls = await form.evaluate((element) => {
const formRect = element.getBoundingClientRect();
const controls = Array.from(element.querySelectorAll<HTMLElement>("input, select, button"));
const controls = Array.from(
element.querySelectorAll<HTMLElement>("input, select, output, button"),
);
return controls
.map((control) => {
@ -1398,7 +1401,7 @@ test.describe("admin route", () => {
await expectNoHorizontalOverflow(page);
});
test("allows an admin to manage real server-known users", async ({ page }, testInfo) => {
test("allows an admin to manage real server-known users", async ({ page, browser }, testInfo) => {
test.setTimeout(60_000);
await signInAsAdmin(page);
@ -1427,16 +1430,19 @@ test.describe("admin route", () => {
await sessionHeader.getByRole("button").click();
await expect(sessionHeader).toHaveAttribute("aria-sort", "descending");
await page.getByRole("tab", { name: "사용자 등록" }).click();
await page.getByRole("tab", { name: "외부 연구참여자 사전등록" }).click();
await page.getByLabel("새 사용자 이메일").fill(email);
await page.getByLabel("새 사용자 표시 이름").fill(displayName);
await page.getByLabel("새 사용자 역할").selectOption("learner");
await page.getByLabel("새 사용자 승인 상태").selectOption("pending");
await expect(page.getByLabel("새 사용자 승인 상태")).toHaveAttribute(
"data-value",
"pending",
);
await page.getByLabel("새 사용자 코호트").fill(`created-${testInfo.project.name}`);
const createPromise = page.waitForResponse(isAdminUserCreate);
const reloadAfterCreatePromise = page.waitForResponse(isAdminUsersResponse);
await page.getByRole("button", { name: "사용자 등록" }).click();
await page.getByRole("button", { name: "연구참여자 사전등록" }).click();
const createResponse = await createPromise;
await expectResponseOk(createResponse);
await expectResponseOk(await reloadAfterCreatePromise);
@ -1461,17 +1467,54 @@ test.describe("admin route", () => {
await expect(approvalCard).toContainText("승인 대기");
await expectVisibleButtonsFit(page, ".vgops-approval__actions .vg-btn", "admin approval buttons");
const approvePromise = page.waitForResponse(isAdminUserPatch(created.user_id));
await approvalCard.getByRole("button", { name: "승인" }).click();
const approveResponse = await approvePromise;
await expectResponseOk(approveResponse);
const approved = (await approveResponse.json()) as AdminManagedUser;
expect(approved).toMatchObject({
user_id: created.user_id,
email,
account_status: "approved",
const pendingContext = await browser.newContext({
baseURL: String(testInfo.project.use.baseURL),
});
await expect(approvalCard).toHaveCount(0);
try {
const pendingPage = await pendingContext.newPage();
const pendingLogin = await pendingPage.request.post("/api/auth/dev-login", {
data: {
email,
role: "learner",
display_name: displayName,
},
});
await expectResponseOk(pendingLogin);
expect(await pendingLogin.json()).toMatchObject({
email,
account_status: "pending",
approval_required: true,
});
const blockedPersonas = await pendingPage.request.get("/api/personas");
expect(blockedPersonas.status()).toBe(403);
expect(await blockedPersonas.json()).toMatchObject({ detail: "account_pending" });
await pendingPage.goto("/learn");
await expect(pendingPage).toHaveURL(/\/pending$/);
const approvePromise = page.waitForResponse(isAdminUserPatch(created.user_id));
await approvalCard.getByRole("button", { name: "승인" }).click();
const approveResponse = await approvePromise;
await expectResponseOk(approveResponse);
const approved = (await approveResponse.json()) as AdminManagedUser;
expect(approved).toMatchObject({
user_id: created.user_id,
email,
account_status: "approved",
});
await expect(approvalCard).toHaveCount(0);
const approvedMe = await pendingPage.request.get("/api/auth/me");
await expectResponseOk(approvedMe);
expect(await approvedMe.json()).toMatchObject({
email,
account_status: "approved",
approval_required: false,
});
await expectResponseOk(await pendingPage.request.get("/api/personas"));
} finally {
await pendingContext.close();
}
await page.getByRole("tab", { name: "사용자 목록" }).click();
await page.getByLabel("사용자 검색").fill(email);
@ -1484,6 +1527,7 @@ test.describe("admin route", () => {
const nextName = `교수자 ${testInfo.project.name}`;
const nameInput = card.getByLabel(`${email} 표시 이름`);
const roleSelect = card.getByLabel(`${email} 역할`);
const feedbackToggle = card.getByLabel(`${email} 학습자 AI 피드백`);
const cohortInput = card.getByLabel(`${email} 코호트`);
const saveButton = card.getByRole("button", { name: "저장" });
const nextCohort = `cohort-${testInfo.project.name}`;
@ -1492,6 +1536,9 @@ test.describe("admin route", () => {
await page.evaluate(() => new Promise(requestAnimationFrame));
await roleSelect.selectOption("teacher");
await expect(roleSelect).toHaveValue("teacher");
await expect(feedbackToggle).toBeChecked();
await feedbackToggle.uncheck();
await expect(feedbackToggle).not.toBeChecked();
await cohortInput.fill(nextCohort);
await expect(cohortInput).toHaveValue(nextCohort);
await page.evaluate(() => new Promise(requestAnimationFrame));
@ -1507,14 +1554,23 @@ test.describe("admin route", () => {
email,
display_name: nextName,
role: "teacher",
learner_feedback_enabled: false,
account_status: "approved",
cohort_ids: [nextCohort],
});
await expect(nameInput).toHaveValue(nextName);
await expect(card).toContainText("교수자");
await expect(feedbackToggle).not.toBeChecked();
await expect(cohortInput).toHaveValue(nextCohort);
const persistedUsersResponse = await page.request.get("/api/admin/users");
await expectResponseOk(persistedUsersResponse);
const persistedUsers = (await persistedUsersResponse.json()) as AdminUsersResponse;
expect(
persistedUsers.users.find((user) => user.user_id === created.user_id),
).toMatchObject({ learner_feedback_enabled: false });
const deletePromise = page.waitForResponse(isAdminUserDelete(created.user_id));
await card.getByRole("button", { name: "비활성화" }).click();
const deleteResponse = await deletePromise;
@ -1663,7 +1719,7 @@ test.describe("admin route", () => {
await signInAsAdmin(page);
const users = await openAdminAndReadUsers(page);
await page.getByRole("tab", { name: "사용자 등록" }).click();
await page.getByRole("tab", { name: "외부 연구참여자 사전등록" }).click();
const layout = await page.evaluate(() => {
const form = document.querySelector<HTMLElement>(".vgops-user-create");
if (!form) throw new Error("admin user create form was not rendered");
@ -1702,7 +1758,7 @@ test.describe("admin route", () => {
for (const width of [861, 900, 1024]) {
await page.setViewportSize({ width, height: 900 });
await openAdminAndReadUsers(page);
await page.getByRole("tab", { name: "사용자 등록" }).click();
await page.getByRole("tab", { name: "외부 연구참여자 사전등록" }).click();
await expectNoHorizontalOverflow(page);
await expectCreateUserControlsFit(page, width);