전 저장소 리팩터링과 SSOT 정비

This commit is contained in:
Yun Chan 2026-07-15 21:31:30 +09:00
parent 14ecbd4e7d
commit 3dfddcac6f
173 changed files with 19679 additions and 6952 deletions

View file

@ -106,7 +106,7 @@ async function mockSessionDetail(
test.describe("persona avatar expression rig", () => {
test.beforeEach(async ({ page }) => {
await page.route("**/api/auth/me", (route) =>
await page.route(/\/(?:api\/)?auth\/me(?:\?.*)?$/, (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
@ -122,7 +122,7 @@ test.describe("persona avatar expression rig", () => {
}),
);
await page.route("**/api/personas", (route) =>
await page.route(/\/(?:api\/)?personas(?:\?.*)?$/, (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
@ -137,6 +137,34 @@ test.describe("persona avatar expression rig", () => {
),
}),
);
await page.route(/\/(?:api\/)?voice\/health(?:\?.*)?$/, (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ available: false, reason: "test fixture" }),
}),
);
await page.route(/\/(?:api\/)?sessions\/[^/]+\/live-coach(?:\?.*)?$/, (route) => {
if (route.request().method() === "GET") {
return route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
session_id: "avatar-expression-session",
events: [],
quota: { used: 0, limit: 3, remaining: 3 },
source: "database",
}),
});
}
return route.fulfill({
status: 503,
contentType: "application/json",
body: JSON.stringify({ detail: "live coach is outside this avatar fixture" }),
});
});
});
test("renders every persona with a distinct baseline expression and at least 20 expressions", async ({
@ -158,17 +186,12 @@ test.describe("persona avatar expression rig", () => {
live2dMotionFile: el.getAttribute("data-live2d-motion-file"),
live2dExpressionCount: Number(el.getAttribute("data-live2d-expression-count")),
renderMode: el.getAttribute("data-render-mode"),
rasterArtSet: el.querySelector(".vg-raster")?.getAttribute("data-raster-art-set"),
rasterMode: el.querySelector(".vg-raster")?.getAttribute("data-raster-mode"),
rasterLayerCount: el.querySelectorAll(".vg-raster__layer").length,
rasterBox: el.querySelector<HTMLElement>(".vg-raster")?.getBoundingClientRect().toJSON(),
rasterCount: el.querySelectorAll(".vg-raster").length,
primitiveCount: el.querySelectorAll("svg path, svg ellipse, svg circle, svg line, svg rect")
.length,
neckBox: el.querySelector<SVGGraphicsElement>('[data-avatar-neck="true"]')?.getBoundingClientRect().toJSON(),
svgBox: el.querySelector("svg")?.getBoundingClientRect().toJSON(),
}));
const expectedArtSet = `${persona.code.toLowerCase()}-live2d-generated`;
expect(metrics.expressionCount, `${persona.code} expression count`).toBeGreaterThanOrEqual(20);
expect(metrics.live2dSchema, `${persona.code} Live2D schema`).toBe("vignette.live2d.v1");
expect(metrics.live2dModel, `${persona.code} Live2D model`).toBe(persona.expectedModel);
@ -180,15 +203,12 @@ test.describe("persona avatar expression rig", () => {
`expressions/${persona.expectedExpression}.exp3.json`,
);
expect(metrics.live2dExpressionCount, `${persona.code} Live2D expressions`).toBeGreaterThanOrEqual(20);
expect(metrics.renderMode, `${persona.code} render mode`).toBe("raster");
expect(metrics.rasterArtSet, `${persona.code} generated art set`).toBe(expectedArtSet);
expect(metrics.rasterMode, `${persona.code} raster rig mode`).toBe("psb-detailed");
expect(metrics.rasterLayerCount, `${persona.code} raster layers`).toBeGreaterThanOrEqual(20);
expect(metrics.rasterBox?.width, `${persona.code} raster width`).toBeGreaterThan(0);
expect(metrics.rasterBox?.height, `${persona.code} raster height`).toBeGreaterThan(0);
expect(metrics.primitiveCount, `${persona.code} fallback SVG primitives`).toBe(0);
expect(metrics.neckBox, `${persona.code} SVG neck fallback`).toBeUndefined();
expect(metrics.svgBox, `${persona.code} SVG fallback`).toBeUndefined();
expect(metrics.renderMode, `${persona.code} render mode`).toBe("svg");
expect(metrics.rasterCount, `${persona.code} raster rig should be disabled`).toBe(0);
expect(metrics.primitiveCount, `${persona.code} SVG primitives`).toBeGreaterThan(10);
expect(metrics.neckBox?.width, `${persona.code} SVG neck`).toBeGreaterThan(0);
expect(metrics.svgBox?.width, `${persona.code} SVG rig`).toBeGreaterThan(0);
expect(metrics.svgBox?.height, `${persona.code} SVG rig`).toBeGreaterThan(0);
}
});
@ -221,8 +241,9 @@ test.describe("persona avatar expression rig", () => {
const activeAvatar = page.locator(".sx-page--active .vg-avatar").first();
await expect(activeAvatar).toBeVisible();
await expect(activeAvatar).toHaveAttribute("data-render-mode", "raster");
await expect(activeAvatar.locator('.vg-raster[data-raster-art-set="p4-live2d-generated"]')).toBeVisible();
await expect(activeAvatar).toHaveAttribute("data-render-mode", "svg");
await expect(activeAvatar.locator(".vg-raster")).toHaveCount(0);
await expect(activeAvatar.locator(".vg-avatar__svg")).toBeVisible();
await expect(activeAvatar).toHaveAttribute("data-expression-count", "28");
await expect(activeAvatar).toHaveAttribute("data-live2d-expression-count", "28");
await expect(activeAvatar).toHaveAttribute("data-live2d-model", "vignette-p4-live2d");
@ -275,8 +296,9 @@ test.describe("persona avatar expression rig", () => {
await page.getByRole("button", { name: "회기 시작" }).click();
await expect(page.locator(".sx-page--active")).toBeVisible();
const activeAvatar = page.locator(".sx-page--active .vg-avatar").first();
await expect(activeAvatar).toHaveAttribute("data-render-mode", "raster");
await expect(activeAvatar.locator('.vg-raster[data-raster-art-set="p4-live2d-generated"]')).toBeVisible();
await expect(activeAvatar).toHaveAttribute("data-render-mode", "svg");
await expect(activeAvatar.locator(".vg-raster")).toHaveCount(0);
await expect(activeAvatar.locator(".vg-avatar__svg")).toBeVisible();
await expect(activeAvatar).toHaveAttribute("data-live2d-motion", "anxious");
await setLearnerUtterance(page, "조금 안정된 것 같아요.");
@ -295,18 +317,15 @@ test.describe("persona avatar expression rig", () => {
await expect(page.locator(".sx-stage__now")).toContainText("온화함");
});
test("uses the PSD v2 crying raster rig for P1 Seoyeon", async ({ page }) => {
test("uses the original SVG parameter rig for P1 Seoyeon", async ({ page }) => {
await page.goto("/learn/session/P1");
const avatar = page.locator('.vg-avatar[data-persona-code="P1"]').first();
await expect(avatar).toBeVisible();
await expect(avatar).toHaveAttribute("data-render-mode", "raster");
await expect(avatar).toHaveAttribute("data-render-mode", "svg");
await expect(avatar).toHaveAttribute("data-affect", "sad");
const raster = avatar.locator('.vg-raster[data-raster-art-set="seoyeon-live2d-psd-v2"]');
await expect(raster).toBeVisible();
await expect(raster).toHaveAttribute("data-raster-variant", "sad");
await expect(raster.locator('.vg-raster__layer--tear[src$="/tear-left.png"]')).toHaveCount(1);
await expect(raster.locator('.vg-raster__layer--tear[src$="/tear-right.png"]')).toHaveCount(1);
await expect(avatar.locator(".vg-raster")).toHaveCount(0);
await expect(avatar.locator(".vg-avatar__svg")).toBeVisible();
await expect(avatar.locator('[data-avatar-neck="true"]')).toBeVisible();
});
});