운영 화면과 회기 리뷰 UI 갱신

This commit is contained in:
Yun Chan 2026-06-28 20:13:21 +09:00
parent e7ebb38177
commit 3a9f70a97b
18 changed files with 2210 additions and 137 deletions

View file

@ -29,6 +29,17 @@ interface UserPreferencesResponse {
};
}
interface UserSupportTicketsResponse {
source: "database";
durable: boolean;
tickets: Array<{
ticket_id: string;
status: string;
subject: string;
resolution_note: string;
}>;
}
interface AdminEngineConfigResponse {
engine_mode: string;
engine_url: string;
@ -214,6 +225,9 @@ async function expectNoSettingsControlClipping(page: Page) {
".vg-set__field",
".vg-set__control-block",
".vg-set__opt",
".vg-set__tickets",
".vg-set__ticket",
".vg-set__ticket-note",
".vg-set__voice",
".vg-set__range-row",
".vg-set__seg",
@ -405,27 +419,39 @@ async function openSettings(page: Page, options: { admin?: boolean } = {}) {
const voicePresetsPromise = page.waitForResponse(
isApiResponse("GET", "/users/me/voice-presets"),
);
const supportTicketsPromise = page.waitForResponse(
isApiResponse("GET", "/users/support-tickets"),
);
const engineConfigPromise = options.admin
? page.waitForResponse(isApiResponse("GET", "/admin/engine-config"))
: null;
await page.goto("/settings");
const [profileResponse, preferencesResponse, voicePresetsResponse, engineConfigResponse] =
const [
profileResponse,
preferencesResponse,
voicePresetsResponse,
supportTicketsResponse,
engineConfigResponse,
] =
await Promise.all([
profilePromise,
preferencesPromise,
voicePresetsPromise,
supportTicketsPromise,
engineConfigPromise,
]);
await expectResponseOk(profileResponse);
await expectResponseOk(preferencesResponse);
await expectResponseOk(voicePresetsResponse);
await expectResponseOk(supportTicketsResponse);
if (engineConfigResponse) await expectResponseOk(engineConfigResponse);
const profile = (await profileResponse.json()) as UserProfileResponse;
const preferences = (await preferencesResponse.json()) as UserPreferencesResponse;
const supportTickets = (await supportTicketsResponse.json()) as UserSupportTicketsResponse;
const engineConfig = engineConfigResponse
? ((await engineConfigResponse.json()) as AdminEngineConfigResponse)
: null;
@ -433,7 +459,7 @@ async function openSettings(page: Page, options: { admin?: boolean } = {}) {
await expect(page).toHaveURL(/\/settings$/);
await expect(page.locator(".vg-set")).toBeVisible();
return { profile, preferences, engineConfig };
return { profile, preferences, supportTickets, engineConfig };
}
test.describe("settings page", () => {
@ -580,6 +606,55 @@ test.describe("settings page", () => {
expect(preferences.notifications.product_news).toBe(nextProductNews);
});
test("learner sees only own support ticket status and resolution note", async ({
page,
}, testInfo) => {
const learnerName = `Settings Ticket Learner ${testInfo.project.name}`;
const learnerEmail = await signInAs(page, "learner", testInfo, learnerName);
const subject = `설정 티켓 상태 ${slugFor(testInfo)}`;
const hiddenBody = `설정 화면에는 보이면 안 되는 문의 본문 ${slugFor(testInfo)}`;
const create = await page.request.post("/api/users/support-tickets", {
data: {
category: "session_review",
priority: "high",
subject,
body: hiddenBody,
source_path: "/settings",
},
});
await expectResponseOk(create);
const created = (await create.json()) as { ticket_id: string };
await signInAs(
page,
"admin",
testInfo,
`Settings Ticket Admin ${testInfo.project.name}`,
`settings.ticket.admin.${slugFor(testInfo)}@twentyoz.kr`,
);
const resolutionNote = `재처리 완료 ${slugFor(testInfo)}`;
const patch = await page.request.patch(`/api/admin/tickets/${created.ticket_id}`, {
data: {
status: "resolved",
assigned_group: "서비스 운영자",
resolution_note: resolutionNote,
},
});
await expectResponseOk(patch);
await signInAs(page, "learner", testInfo, learnerName, learnerEmail);
const { supportTickets } = await openSettings(page);
expect(supportTickets.tickets.some((ticket) => ticket.ticket_id === created.ticket_id)).toBe(
true,
);
const support = page.locator("#set-support");
await expect(support).toContainText(subject);
await expect(support).toContainText("해결");
await expect(support).toContainText(resolutionNote);
await expect(support).not.toContainText(hiddenBody);
});
test("admin sees and updates the AI engine settings panel @single-run", async ({ page }, testInfo) => {
const displayName = `Settings Admin ${testInfo.project.name}`;
const email = await signInAs(page, "admin", testInfo, displayName);