전 저장소 리팩터링과 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

@ -97,63 +97,79 @@ async function createEndedSession(page: Page) {
return session.session_id;
}
/** 리뷰는 모든 폭에서 가로 탭으로 영역을 전환한다. */
async function openReviewTab(page: Page, label: "축어록" | "피드백" | "워크시트") {
// 탭 바는 리뷰 레이아웃이 로드된 뒤에만 DOM에 존재한다 — attach를 먼저 기다린다.
const tabs = page.locator(".sr-tabs");
await tabs.waitFor({ state: "attached", timeout: 10_000 }).catch(() => {});
const tab = tabs.locator("button", { hasText: label });
if (await tab.isVisible().catch(() => false)) {
await tab.click();
}
}
async function expectReviewRegionsReachable(page: Page) {
await expect(page.locator(".sr-overview")).toBeVisible();
await expect(page.locator(".sr-tabs")).toBeVisible();
// 피드백 탭: 인사이트 흐름 + 평가 레일이 함께 도달 가능해야 한다.
await openReviewTab(page, "피드백");
await expect(page.locator(".sr-feedback")).toBeVisible();
await expect(page.locator(".sr-card--chart")).toBeVisible();
await expect(page.locator(".sr-card--flow")).toBeVisible();
await expect(page.locator(".sr-card--rubric")).toBeVisible();
await expect(page.locator(".sr-card--transcript")).toBeVisible();
await expect(page.locator(".sr-card--prepost")).toBeVisible();
const grid = await page.locator(".sr-cols").evaluate((el) => {
const insightsGrid = await page.locator(".sr-cols").evaluate((el) => {
const style = window.getComputedStyle(el);
return {
display: style.display,
areas: style.gridTemplateAreas,
columns: style.gridTemplateColumns.split(" ").filter(Boolean).length,
};
});
expect(grid.display).toBe("grid");
expect(grid.areas).toContain("overview");
expect(grid.areas).toContain("feedback");
expect(grid.areas).toContain("transcript");
expect(grid.areas).not.toContain("teacher");
expect(insightsGrid.display).toBe("grid");
expect(insightsGrid.areas).toContain("insight");
expect(insightsGrid.areas).toContain("feedback");
expect(insightsGrid.areas).not.toContain("teacher");
// 전 폭 탭 계약: 활성 탭 하나가 전폭 단일 컬럼을 쓴다(인사이트 위 / 평가 아래).
expect(insightsGrid.columns).toBe(1);
const stackGeometry = await page.locator(".sr-cols").evaluate((el) => {
const insight = el.querySelector(".sr-left")?.getBoundingClientRect();
const feedback = el.querySelector(".sr-right")?.getBoundingClientRect();
return {
insightBottom: insight ? Math.ceil(insight.bottom) : 0,
feedbackTop: feedback ? Math.floor(feedback.top) : 0,
};
});
expect(stackGeometry.insightBottom).toBeLessThanOrEqual(stackGeometry.feedbackTop + 1);
// 워크시트 탭.
await openReviewTab(page, "워크시트");
await expect(page.locator(".sr-card--worksheet")).toBeVisible();
// 축어록 탭(기본)으로 복귀 — 축어록은 전폭 단일 컬럼.
await openReviewTab(page, "축어록");
await expect(page.locator(".sr-card--transcript")).toBeVisible();
const transcriptAreas = await page
.locator(".sr-cols")
.evaluate((el) => window.getComputedStyle(el).gridTemplateAreas);
expect(transcriptAreas).toContain("transcript");
await expect(page.locator(".sr-overview")).toBeInViewport();
await page.locator(".sr-card--transcript").scrollIntoViewIfNeeded();
await expect(page.locator(".sr-card--transcript")).toBeInViewport();
const railGeometry = await page.locator(".sr-cols").evaluate((gridEl) => {
const root = gridEl.closest(".sr-root");
const overview = gridEl.querySelector(".sr-overview")?.getBoundingClientRect();
const transcript = gridEl.querySelector(".sr-card--transcript")?.getBoundingClientRect();
const rubric = gridEl.querySelector(".sr-card--rubric")?.getBoundingClientRect();
const prepost = gridEl.querySelector(".sr-card--prepost")?.getBoundingClientRect();
const worksheet = gridEl.querySelector(".sr-card--worksheet")?.getBoundingClientRect();
const columns = window
.getComputedStyle(gridEl)
.gridTemplateColumns.split(" ")
.filter(Boolean).length;
// 리뷰 요약은 본문 그리드 위의 전폭 밴드다(도킹/스크롤 부유 금지).
const band = await page.locator(".sr-root").evaluate((rootEl) => {
const overview = rootEl.querySelector(".sr-overview")?.getBoundingClientRect();
const gridRect = rootEl.querySelector(".sr-cols")?.getBoundingClientRect();
return {
isEmpty: root?.classList.contains("sr-root--empty") ?? false,
columns,
overviewLeft: overview ? Math.floor(overview.left) : 0,
overviewRight: overview ? Math.ceil(overview.right) : 0,
transcriptLeft: transcript ? Math.floor(transcript.left) : 0,
transcriptRight: transcript ? Math.ceil(transcript.right) : 0,
rubricLeft: rubric ? Math.floor(rubric.left) : 0,
prepostLeft: prepost ? Math.floor(prepost.left) : 0,
worksheetLeft: worksheet ? Math.floor(worksheet.left) : 0,
overviewBottom: overview ? Math.ceil(overview.bottom) : 0,
gridTop: gridRect ? Math.floor(gridRect.top) : 0,
};
});
if (!railGeometry.isEmpty && railGeometry.columns === 2) {
expect(railGeometry.transcriptRight).toBeLessThanOrEqual(railGeometry.overviewLeft);
expect(railGeometry.rubricLeft).toBeGreaterThanOrEqual(railGeometry.overviewLeft);
expect(railGeometry.prepostLeft).toBeGreaterThanOrEqual(railGeometry.overviewLeft);
expect(railGeometry.worksheetLeft).toBeGreaterThanOrEqual(railGeometry.overviewLeft);
} else if (railGeometry.columns >= 3) {
expect(railGeometry.overviewRight).toBeLessThanOrEqual(railGeometry.transcriptLeft);
}
expect(band.overviewBottom).toBeLessThanOrEqual(band.gridTop + 1);
}
test.describe("session review", () => {
@ -186,6 +202,7 @@ test.describe("session review", () => {
await page.goto(`/learn/session/${sessionId}/review`);
await expect(page).toHaveURL(new RegExp(`/teach/session/${sessionId}/review$`));
await openReviewTab(page, "피드백");
await expect(page.getByText("교수자 검토", { exact: true })).toBeVisible();
expect(reviewAttempts).toBeGreaterThanOrEqual(2);
});
@ -221,6 +238,7 @@ test.describe("session review", () => {
await page.goto(`/learn/session/${sessionId}/review`);
await expect(page).toHaveURL(new RegExp(`/teach/session/${sessionId}/review$`));
await openReviewTab(page, "피드백");
await expect(page.getByText("교수자 검토", { exact: true })).toBeVisible();
await expect(page.locator(".sr-card--prepost")).toHaveCount(0);
expect(reviewAttempts).toBeGreaterThanOrEqual(2);
@ -233,6 +251,7 @@ test.describe("session review", () => {
await page.goto(`/teach/session/${sessionId}/review`);
await openReviewTab(page, "피드백");
await expect(page.getByText("AI 평가 범위", { exact: true })).toBeVisible();
await expect(page.getByText("현재 구현", { exact: true })).toBeVisible();
await expect(page.getByText(/fast-loop.*deep-loop/)).toBeVisible();
@ -348,6 +367,7 @@ test.describe("session review", () => {
await page.goto(`/teach/session/${sessionId}/review`);
await openReviewTab(page, "피드백");
await expect(page.getByText("평가 대기", { exact: true })).toBeVisible();
await expect(page.getByText("평가 AI가 저장된 축어록을 분석 중입니다.")).toBeVisible();
await expect(page.getByRole("button", { name: "AI 평가 재시도" })).toHaveCount(0);
@ -413,6 +433,7 @@ test.describe("session review", () => {
});
await page.goto(`/teach/session/${sessionId}/review`);
await openReviewTab(page, "피드백");
await expect(page.getByText("평가 실패")).toBeVisible();
await expect(
page.getByText("저장된 축어록은 확인했지만 deep-loop 평가 AI 산출물을 표시하지 못했습니다. AI 평가 재시도가 필요합니다."),
@ -507,6 +528,7 @@ test.describe("session review", () => {
await expect(page).toHaveURL(new RegExp(`/learn/session/${sessionId}/review$`));
await expect(page.getByText("축어록 없음")).toBeVisible();
await openReviewTab(page, "피드백");
await expect(page.getByText("감정 타임라인 대기")).toBeVisible();
await expect(page.getByText("개선점 대기")).toBeVisible();
await expect(page.getByRole("button", { name: "오디오 다시 듣기" })).toBeDisabled();
@ -547,9 +569,12 @@ test.describe("session review", () => {
await expect(page).toHaveURL(new RegExp(`/learn/session/${FILLED_REVIEW_SESSION_ID}/review$`));
await expect(page.getByText("관계 신호가 안정적으로 회복된 회기")).toBeVisible();
await expect(page.getByText("세션 트랜스크립트")).toBeVisible();
await expect(page.getByText("감정 반영").first()).toBeVisible();
await expect(page.getByText("AI 슈퍼바이저").first()).toBeVisible();
await openReviewTab(page, "피드백");
await expect(page.getByText("감정 밸런스 타임라인")).toBeVisible();
await expect(page.getByText("회기 흐름")).toBeVisible();
await expect(page.getByText("세션 트랜스크립트")).toBeVisible();
await expect(page.getByText("기법 사용 분포")).toBeVisible();
await expect(page.getByText("파일럿 증거 원장")).toBeVisible();
await expect(page.getByText("1/3 쌍")).toBeVisible();
@ -569,13 +594,12 @@ test.describe("session review", () => {
"수련만족도 사후 점수",
]);
await expect(page.getByText("효과 판정과 통계 검정은 평가설계 확정 후 별도 산출합니다.")).toBeVisible();
await expect(page.getByText("사례개념화 워크시트")).toBeVisible();
await expect(page.getByText("감정 반영").first()).toBeVisible();
await expect(page.getByText("AI 슈퍼바이저").first()).toBeVisible();
await expect(page.getByText("다음에 시도할 문장")).toBeVisible();
await expect(page.getByText("방어를 낮춘 감정 반영")).toBeVisible();
await expect(page.getByText("개입 전 준비도 확인")).toBeVisible();
await expect(page.locator(".sr-chart__svg")).toBeVisible();
await openReviewTab(page, "워크시트");
await expect(page.getByText("사례개념화 워크시트")).toBeVisible();
await expect(page.locator(".sr-ws-input").first()).toBeVisible();
await expect(page.getByRole("button", { name: "오디오 다시 듣기" })).toBeEnabled();
await expect(page.getByRole("button", { name: "PDF 내보내기" })).toBeEnabled();
@ -624,6 +648,7 @@ test.describe("session review", () => {
"저는 익명 내담자이고 소속 기관에서 상담받고 있어요.",
);
await expect(transcript).not.toContainText("[NAME]");
await openReviewTab(page, "워크시트");
await expect(page.locator(".sr-ws-evidence").filter({ hasText: "[NAME]" })).toBeVisible();
});
@ -633,6 +658,8 @@ test.describe("session review", () => {
await routePrepostMeasures(page);
await page.goto(`/learn/session/${FILLED_REVIEW_SESSION_ID}/review`);
await expect(page.locator(".sr-overview")).toBeVisible({ timeout: 15_000 });
await openReviewTab(page, "피드백");
await expect(page.locator(".sr-card--prepost")).toBeVisible({ timeout: 15_000 });
const prepostInputs = page.locator(".sr-card--prepost input");