diff --git a/apps/api/app/services/supervision_research_producer.py b/apps/api/app/services/supervision_research_producer.py index 2406794..8ed7216 100644 --- a/apps/api/app/services/supervision_research_producer.py +++ b/apps/api/app/services/supervision_research_producer.py @@ -81,7 +81,7 @@ def _signal_id(*parts: object) -> str: def _route(session_id: object) -> str: - return f"/teacher/sessions/{session_id}" + return f"/teach/session/{session_id}/review" def _pointer( @@ -94,7 +94,7 @@ def _pointer( ledger=ledger, event_id=str(event_id), session_id=str(session_id) if session_id else None, - route_hint=_route(session_id) if session_id else "/teacher/dashboard", + route_hint=_route(session_id) if session_id else "/teach", ) diff --git a/apps/api/app/session_dashboard_projection.py b/apps/api/app/session_dashboard_projection.py index 86c5c6f..20b70a7 100644 --- a/apps/api/app/session_dashboard_projection.py +++ b/apps/api/app/session_dashboard_projection.py @@ -301,10 +301,15 @@ def dashboard_achievements( sessions: list[InProcSession], review_ready: dict[str, bool], ) -> list[LearnerDashboardAchievement]: - completed = sum(1 for sess in sessions if sess.ended) + practice_sessions = [ + sess + for sess in sessions + if any(turn.speaker in ("counselor", "learner") for turn in sess.turns) + ] + completed = sum(1 for sess in practice_sessions if sess.ended) active = sum(1 for sess in sessions if not sess.ended) review_count = sum(1 for ready in review_ready.values() if ready) - by_persona = Counter(sess.persona_code for sess in sessions) + by_persona = Counter(sess.persona_code for sess in practice_sessions) max_persona_sessions = max(by_persona.values(), default=0) persona_coverage = len(by_persona) return [ diff --git a/apps/api/app/test_dashboard_projection_tdd.py b/apps/api/app/test_dashboard_projection_tdd.py index d4f3f16..51fb79c 100644 --- a/apps/api/app/test_dashboard_projection_tdd.py +++ b/apps/api/app/test_dashboard_projection_tdd.py @@ -6,8 +6,9 @@ from uuid import uuid4 from app.deps import Principal, Role from app.services import session_metrics, state_machine -from app.services.persona import P1, P2 +from app.services.persona import P1, P2, P3 from app.session_dashboard_projection import ( + dashboard_achievements, dashboard_overview, dashboard_persona_progress, dashboard_training_exposure, @@ -23,7 +24,7 @@ def _make_session( ended: bool = True, turns: list[TurnRecord] | None = None, ) -> InProcSession: - persona = {P1.code: P1, P2.code: P2}.get(persona_code, P1) + persona = {P1.code: P1, P2.code: P2, P3.code: P3}.get(persona_code, P1) state = state_machine.init_state(params=persona.openness_params()) state.stage = state_machine.Stage.EXPLORE sess = InProcSession( @@ -118,6 +119,121 @@ class DashboardProjectionTDDTests(unittest.TestCase): self.assertEqual(p2_row.sessions, 1) self.assertEqual(p2_row.completed_sessions, 1) + def test_achievements_ignore_zero_turn_ended_sessions(self) -> None: + """무발화 종료 회기는 성취를 열지 않는다.""" + learner_id = "00000000-0000-0000-0000-000000000111" + zero_turn = _make_session("sess-zero", 1, learner_id, ended=True, turns=[]) + + achievements = { + item.id: item + for item in dashboard_achievements( + [zero_turn], + {}, + ) + } + + self.assertEqual(achievements["first_session_complete"].state, "locked") + self.assertEqual(achievements["review_ready"].state, "locked") + self.assertEqual(achievements["review_ready"].detail, "현재 리뷰 가능한 회기 0건입니다.") + + def test_achievements_ignore_zero_turn_repeat_and_coverage(self) -> None: + """무발화 회기는 반복 및 페르소나 연습 기록으로 집계하지 않는다.""" + learner_id = "00000000-0000-0000-0000-000000000111" + zero_turns = [ + _make_session( + "sess-zero-1", 1, learner_id, ended=True, turns=[] + ), + _make_session( + "sess-zero-2", 2, learner_id, persona_code="P2", ended=True, turns=[] + ), + _make_session( + "sess-zero-3", 3, learner_id, persona_code="P3", ended=True, turns=[] + ), + ] + + achievements = { + item.id: item + for item in dashboard_achievements(zero_turns, {}) + } + + self.assertEqual(achievements["persona_repeat"].state, "locked") + self.assertEqual(achievements["persona_coverage"].state, "locked") + + def test_achievements_ignore_client_only_ended_session(self) -> None: + """내담자 발화만 있는 종료 회기는 학습 성취를 만들지 않는다.""" + learner_id = "00000000-0000-0000-0000-000000000111" + client_only = _make_session("sess-client-only", 1, learner_id, ended=True) + client_only.turns[0].speaker = "client" + + achievements = { + item.id: item + for item in dashboard_achievements( + [client_only], + {}, + ) + } + + self.assertEqual(achievements["first_session_complete"].state, "locked") + self.assertEqual(achievements["review_ready"].state, "locked") + self.assertEqual(achievements["persona_repeat"].state, "locked") + + def test_achievements_keep_dialogue_sessions_and_active_progress(self) -> None: + """학습자 발화와 진행 중 회기의 기존 성취 흐름을 유지한다.""" + learner_id = "00000000-0000-0000-0000-000000000111" + completed = [ + _make_session(f"sess-p1-{index}", index, learner_id, ended=True) + for index in range(1, 4) + ] + completed.extend( + [ + _make_session("sess-p2", 4, learner_id, persona_code="P2", ended=True), + _make_session("sess-p3", 5, learner_id, persona_code="P3", ended=True), + ] + ) + active = _make_session("sess-active", 6, learner_id, ended=False) + + achievements = { + item.id: item + for item in dashboard_achievements( + [*completed, active], + {completed[0].session_id: True}, + ) + } + + self.assertEqual(achievements["first_session_complete"].state, "done") + self.assertEqual(achievements["review_ready"].state, "done") + self.assertEqual(achievements["persona_repeat"].state, "done") + self.assertEqual(achievements["persona_coverage"].state, "done") + + active_only = { + item.id: item + for item in dashboard_achievements([active], {}) + } + self.assertEqual(active_only["first_session_complete"].state, "available") + self.assertEqual(active_only["persona_repeat"].state, "available") + + learner_alias = _make_session("sess-learner", 7, learner_id, ended=True) + learner_alias.turns[0].speaker = "learner" + learner_alias_achievements = { + item.id: item + for item in dashboard_achievements( + [learner_alias], + {learner_alias.session_id: True}, + ) + } + self.assertEqual(learner_alias_achievements["first_session_complete"].state, "done") + self.assertEqual(learner_alias_achievements["review_ready"].state, "done") + + empty_active = _make_session( + "sess-empty-active", 8, learner_id, ended=False, turns=[] + ) + empty_active_achievements = { + item.id: item + for item in dashboard_achievements([empty_active], {}) + } + self.assertEqual(empty_active_achievements["first_session_complete"].state, "available") + self.assertEqual(empty_active_achievements["persona_repeat"].state, "locked") + class TeacherDashboardProjectionTDDTests(unittest.IsolatedAsyncioTestCase): """Teacher dashboard projection alignment for zero-turn sessions.""" @@ -212,4 +328,3 @@ class TeacherDashboardProjectionTDDTests(unittest.IsolatedAsyncioTestCase): 1, "Teacher learner analysis summary.ended_sessions must exclude 0-turn ended sessions", ) - diff --git a/apps/api/app/test_supervision_research_producer.py b/apps/api/app/test_supervision_research_producer.py index fb5c6a0..9563979 100644 --- a/apps/api/app/test_supervision_research_producer.py +++ b/apps/api/app/test_supervision_research_producer.py @@ -225,6 +225,66 @@ class SupervisionResearchProducerDerivationTest(TestCase): serialized = str([item.signal.model_dump(mode="json") for item in derived]) self.assertNotIn("transcript", serialized.lower()) + def test_produced_pointer_uses_canonical_session_review_route(self) -> None: + """세션이 있는 생산 신호는 교수자 세션 리뷰 경로를 가리킨다.""" + derived = derive_attention_signals( + trajectory_rows=[ + { + "learner_id": LEARNER_A, + "revision_id": UUID("20000000-0000-0000-0000-000000000003"), + "anchor_session_id": SESSION_A, + "revision_no": 1, + "assessment": { + "sessions": [ + { + "session_no": 1, + "status": "off_track", + "axes": [{"uncertainty": 0.2}], + } + ] + }, + } + ], + rupture_rows=[], + calibration_rows=[], + transfer_rows=[], + practice_rows=[], + ) + + self.assertEqual(len(derived), 1) + self.assertEqual( + derived[0].signal.evidence[0].route_hint, + f"/teach/session/{SESSION_A}/review", + ) + + def test_produced_pointer_without_session_falls_back_to_teach(self) -> None: + """세션 없는 생산 신호는 교수자 첫 화면으로 안전하게 돌아간다.""" + derived = derive_attention_signals( + trajectory_rows=[], + rupture_rows=[], + calibration_rows=[], + transfer_rows=[ + { + "learner_id": LEARNER_A, + "competency_id": "competency.empathy", + "transfer_assessment_id": UUID( + "50000000-0000-0000-0000-000000000003" + ), + "session_id": None, + "assessment_payload": { + "eligible": True, + "transfer_verified": False, + "error_interval": {"lower": 0.1, "upper": 0.3}, + }, + "observed_sequence": 1, + } + ], + practice_rows=[], + ) + + self.assertEqual(len(derived), 1) + self.assertEqual(derived[0].signal.evidence[0].route_hint, "/teach") + def test_requires_repeated_overconfidence_and_retry_for_persistent_signals( self, ) -> None: diff --git a/apps/web/e2e/calibration-transfer.spec.ts b/apps/web/e2e/calibration-transfer.spec.ts index a156e3a..0e6067a 100644 --- a/apps/web/e2e/calibration-transfer.spec.ts +++ b/apps/web/e2e/calibration-transfer.spec.ts @@ -99,6 +99,23 @@ async function routeReviewShell(page: Page, reviewSessionId = SESSION_ID) { ); } +function routeLearnerReadModel( + page: Page, + expectedSessionIds: readonly string[], + handler: (route: Route) => void | Promise, +) { + return page.route( + (url) => + url.pathname === "/api/calibration/learners/me" && + url.searchParams.has("session_id"), + async (route) => { + const sessionId = new URL(route.request().url()).searchParams.get("session_id"); + expect(expectedSessionIds).toContain(sessionId); + await handler(route); + }, + ); +} + function initialReadModel( requestedView: "learner" | "supervisor" = "learner", ): CalibrationTransferReadModelResponse { @@ -516,7 +533,7 @@ test.describe("G5 calibration mirror and unseen transfer", () => { await routeUnmockedApi(page); await routeAuth(page, "learner"); await routeReviewShell(page); - await page.route("**/api/calibration/learners/me", (route) => + await routeLearnerReadModel(page, [SESSION_ID], (route) => fulfillReadModel(route, readModel), ); await page.route("**/api/calibration/predictions/revisions", async (route) => { @@ -687,7 +704,7 @@ test.describe("G5 calibration mirror and unseen transfer", () => { await routeUnmockedApi(page); await routeAuth(page, "learner"); await routeReviewShell(page); - await page.route("**/api/calibration/learners/me", (route) => + await routeLearnerReadModel(page, [SESSION_ID], (route) => fulfillReadModel(route, readModel), ); @@ -805,7 +822,7 @@ test.describe("G5 calibration mirror and unseen transfer", () => { await routeUnmockedApi(page); await routeAuth(page, "learner"); await routeReviewShell(page, PRACTICE_SESSION_ID); - await page.route("**/api/calibration/learners/me", async (route) => { + await routeLearnerReadModel(page, [PRACTICE_SESSION_ID], async (route) => { readRequests += 1; await fulfillReadModel(route, readModel); }); @@ -935,7 +952,7 @@ test.describe("G5 calibration mirror and unseen transfer", () => { await routeAuth(page, "learner"); await routeReviewShell(page, PRACTICE_SESSION_ID); await routeReviewShell(page, SESSION_ID); - await page.route("**/api/calibration/learners/me", (route) => + await routeLearnerReadModel(page, [SESSION_ID, PRACTICE_SESSION_ID], (route) => fulfillReadModel(route, readModel), ); await page.route("**/api/calibration/transfer-executions", async (route) => { @@ -997,7 +1014,7 @@ test.describe("G5 calibration mirror and unseen transfer", () => { await routeUnmockedApi(page); await routeAuth(page, "learner"); await routeReviewShell(page); - await page.route("**/api/calibration/learners/me", (route) => + await routeLearnerReadModel(page, [SESSION_ID], (route) => fulfillReadModel(route, readModel), ); @@ -1026,7 +1043,7 @@ test.describe("G5 calibration mirror and unseen transfer", () => { await routeUnmockedApi(page); await routeAuth(page, "learner"); await routeReviewShell(page); - await page.route("**/api/calibration/learners/me", async (route) => { + await routeLearnerReadModel(page, [SESSION_ID], async (route) => { if (responseMode === "pending") { await firstResponse; } diff --git a/apps/web/e2e/uc-learner-practice-history.spec.ts b/apps/web/e2e/uc-learner-practice-history.spec.ts index 8a672d9..e41d832 100644 --- a/apps/web/e2e/uc-learner-practice-history.spec.ts +++ b/apps/web/e2e/uc-learner-practice-history.spec.ts @@ -239,8 +239,8 @@ test.describe("uc learner practice/history", () => { "직장 내 대인관계 갈등을 호소", ); - await page.getByRole("button", { name: "새 회기 시작" }).click(); - await expect(page).toHaveURL(/\/learn\/session\/P2$/); + await page.getByRole("button", { name: "완전히 새로 시작" }).click(); + await expect(page).toHaveURL(/\/learn\/session\/P2\?continuity=fresh$/); }); // usecase: 기록이 없는 신규 학습자에게 기초 난도부터 추천하되, 사용 불가 @@ -438,6 +438,96 @@ test.describe("uc learner practice/history", () => { await expect(page).toHaveURL(new RegExp(`/learn/session/${REV_ID}/review$`)); }); + test("검색과 리뷰 필터를 URL에 보존해 리뷰 왕복과 새로고침 뒤에도 복구한다", async ({ + page, + }) => { + await installFixtures(page); + await page.goto("/learn/history?source=dashboard"); + + const search = page.getByLabel("기록 검색"); + const chips = page.locator(".lh-history-filter"); + const historyLength = await page.evaluate(() => window.history.length); + await search.pressSequentially("성하늘"); + await expect(search).toHaveValue("성하늘"); + await expect + .poll(() => new URL(page.url()).searchParams.get("q")) + .toBe("성하늘"); + await expect + .poll(() => page.evaluate(() => window.history.length)) + .toBe(historyLength); + + await chips.getByRole("button", { name: "리뷰 필요" }).click(); + await expect + .poll(() => new URL(page.url()).searchParams.get("status")) + .toBe("review"); + await expect + .poll(() => new URL(page.url()).searchParams.get("source")) + .toBe("dashboard"); + + const revCard = page.locator(".lh-session-card", { + hasText: "AI 피드백 대기", + }); + await revCard.getByRole("button", { name: "리뷰", exact: true }).click(); + await expect(page).toHaveURL(new RegExp(`/learn/session/${REV_ID}/review$`)); + + await page.goBack(); + await expect + .poll(() => new URL(page.url()).searchParams.get("q")) + .toBe("성하늘"); + await expect + .poll(() => new URL(page.url()).searchParams.get("status")) + .toBe("review"); + await expect(search).toHaveValue("성하늘"); + await expect(chips.getByRole("button", { name: "리뷰 필요" })).toHaveAttribute( + "aria-pressed", + "true", + ); + await expect(page.locator(".lh-session-card")).toHaveCount(1); + + await page.reload(); + await expect(search).toHaveValue("성하늘"); + await expect(chips.getByRole("button", { name: "리뷰 필요" })).toHaveAttribute( + "aria-pressed", + "true", + ); + + await search.fill(""); + await chips.getByRole("button", { name: "전체" }).click(); + await expect + .poll(() => new URL(page.url()).searchParams.has("q")) + .toBe(false); + await expect + .poll(() => new URL(page.url()).searchParams.has("status")) + .toBe(false); + await expect + .poll(() => new URL(page.url()).searchParams.get("source")) + .toBe("dashboard"); + }); + + test("잘못된 상태 query는 전체 필터로 안전하게 처리한다", async ({ page }) => { + await installFixtures(page); + await page.goto("/learn/history?status=unknown&q=%EA%B9%80%EB%8F%84%EC%9C%A4&source=dashboard"); + + const chips = page.locator(".lh-history-filter"); + await expect(page.getByLabel("기록 검색")).toHaveValue("김도윤"); + await expect(chips.getByRole("button", { name: "전체" })).toHaveAttribute( + "aria-pressed", + "true", + ); + await expect(page.locator(".lh-session-card")).toHaveCount(1); + + await chips.getByRole("button", { name: "리뷰 필요" }).click(); + await expect + .poll(() => new URL(page.url()).searchParams.get("status")) + .toBe("review"); + await expect + .poll(() => new URL(page.url()).searchParams.get("q")) + .toBe("김도윤"); + await expect + .poll(() => new URL(page.url()).searchParams.get("source")) + .toBe("dashboard"); + }); + // usecase: 종료된 기록 카드에서 다음 회기를 열어 같은 내담자의 맥락을 이어간다. test("종료 기록의 다음 회기 이어가기 버튼이 같은 내담자 새 회기로 이동한다", async ({ page, @@ -450,7 +540,7 @@ test.describe("uc learner practice/history", () => { await endCard .getByRole("button", { name: "다음 회기 이어가기", exact: true }) .click(); - await expect(page).toHaveURL(/\/learn\/session\/P2$/); + await expect(page).toHaveURL(/\/learn\/session\/P2\?continuity=continue$/); }); // usecase: 상태 필터 칩과 검색어를 함께 걸어 조건을 좁히고, 조건이 어긋나면 diff --git a/apps/web/e2e/uc-session-conversation.spec.ts b/apps/web/e2e/uc-session-conversation.spec.ts index 2dc5a49..db7de3f 100644 --- a/apps/web/e2e/uc-session-conversation.spec.ts +++ b/apps/web/e2e/uc-session-conversation.spec.ts @@ -760,9 +760,9 @@ test.describe("회기 대화 — 텍스트 턴과 SSE 스트림", () => { await resumeSession(page); const transcript = page.locator(".sx-transcript__scroll"); - // 조사까지 자연스럽게 바뀐다: [NAME]은→익명 내담자는, [NAME]이→익명 내담자가. - await expect(transcript).toContainText("익명 내담자는 제 얘기를 끝까지 듣지 않아요."); - await expect(transcript).toContainText("익명 내담자가 자꾸 저를 몰아세워요."); + // 조사까지 자연스럽게 바뀐다: [NAME]은→익명 인물은, [NAME]이→익명 인물이. + await expect(transcript).toContainText("익명 인물은 제 얘기를 끝까지 듣지 않아요."); + await expect(transcript).toContainText("익명 인물이 자꾸 저를 몰아세워요."); await expect(transcript).not.toContainText("[NAME]"); }); diff --git a/apps/web/e2e/uc-teacher-console.spec.ts b/apps/web/e2e/uc-teacher-console.spec.ts index 7daa203..c897c05 100644 --- a/apps/web/e2e/uc-teacher-console.spec.ts +++ b/apps/web/e2e/uc-teacher-console.spec.ts @@ -562,6 +562,59 @@ test.describe("uc-teacher-console: 수퍼비전 attention 큐", () => { page.locator(".pf-depth-strip .is-current"), ).toContainText("학습자 상세"); }); + + test("legacy 감독 근거 링크는 엄격히 교사 경로로 정규화하고 canonical 경로는 유지한다", async ({ + page, + }) => { + const legacySessionId = "62000000-0000-0000-0000-000000000801"; + const canonicalSessionId = "62000000-0000-0000-0000-000000000802"; + await routeUnmockedApi(page); + await routeAuthedTeacher(page); + await routeSupervision(page, [ + attentionItemFixture(1, "legacy-session", "growth_stagnation", [ + `/teacher/sessions/${legacySessionId}/`, + ]), + attentionItemFixture(2, "canonical-session", "deterioration", [ + `/teach/session/${canonicalSessionId}/review`, + ]), + attentionItemFixture(3, "legacy-dashboard", "safety_boundary", [ + "/teacher/dashboard", + ]), + ]); + await routeConsole(page, { + pending_reviews: [ + { + ...pendingReviewFixture(1, "62000000-0000-0000-0000-000000000111"), + session_id: legacySessionId, + }, + ], + }); + await routeTeacherSessionReview(page, legacySessionId); + await routeSupervisorCalibration(page, emptyCalibrationReadModelFixture()); + + await page.goto("/teach/supervision"); + + await expect(page.locator(".g6-drilldown a")).toHaveAttribute( + "href", + `/teach/session/${legacySessionId}/review`, + ); + await page.getByRole("button", { name: /canonical-session/ }).click(); + await expect(page.locator(".g6-drilldown a")).toHaveAttribute( + "href", + `/teach/session/${canonicalSessionId}/review`, + ); + await page.getByRole("button", { name: /legacy-dashboard/ }).click(); + await expect(page.locator(".g6-drilldown a")).toHaveAttribute("href", "/teach"); + await page.getByRole("button", { name: /legacy-session/ }).click(); + await page.locator(".g6-drilldown a").click(); + + await expect(page).toHaveURL( + new RegExp(`/teach/session/${legacySessionId}/review$`), + ); + await expect( + page.getByText("교수자 검토 화면", { exact: true }), + ).toBeVisible(); + }); }); test.describe("uc-teacher-console: 학습자 상세 측정 근거", () => { diff --git a/apps/web/src/pages/LearnerHome.tsx b/apps/web/src/pages/LearnerHome.tsx index 256da08..07737f4 100644 --- a/apps/web/src/pages/LearnerHome.tsx +++ b/apps/web/src/pages/LearnerHome.tsx @@ -126,6 +126,10 @@ interface LearnerHomeProps { view?: LearnerHomeView; } +function isHistoryFilter(value: string | null): value is HistoryFilter { + return HISTORY_FILTERS.some((item) => item.value === value); +} + export default function LearnerHome({ view = "dashboard" }: LearnerHomeProps) { const navigate = useNavigate(); const [searchParams] = useSearchParams(); @@ -168,8 +172,18 @@ export default function LearnerHome({ view = "dashboard" }: LearnerHomeProps) { ); const [memoryLoadState, setMemoryLoadState] = useState("idle"); - const [historyFilter, setHistoryFilter] = useState("all"); - const [historyQuery, setHistoryQuery] = useState(""); + const [localHistoryFilter, setLocalHistoryFilter] = + useState("all"); + const [localHistoryQuery, setLocalHistoryQuery] = useState(""); + const historyStatusParam = searchParams.get("status"); + const historyFilter = + view === "history" + ? isHistoryFilter(historyStatusParam) + ? historyStatusParam + : "all" + : localHistoryFilter; + const historyQuery = + view === "history" ? (searchParams.get("q") ?? "") : localHistoryQuery; const [archiveBusyId, setArchiveBusyId] = useState(null); const [archiveNotice, setArchiveNotice] = useState(null); // 중간 폭(≤1180px)에서는 대시보드 섹션을 상단 가로 탭으로 전환한다(2026-07-15 소유자 피드백). @@ -177,6 +191,46 @@ export default function LearnerHome({ view = "dashboard" }: LearnerHomeProps) { const [dashTab, setDashTab] = useState("today"); const dashTabRefs = useRef>([]); + const updateHistorySearchParams = ( + update: (params: URLSearchParams) => void, + ) => { + const params = new URLSearchParams(searchParams); + update(params); + const nextSearch = params.toString(); + navigate( + { search: nextSearch ? `?${nextSearch}` : "" }, + { replace: true }, + ); + }; + + const setHistoryFilter = (nextFilter: HistoryFilter) => { + if (view !== "history") { + setLocalHistoryFilter(nextFilter); + return; + } + updateHistorySearchParams((params) => { + if (nextFilter === "all") { + params.delete("status"); + } else { + params.set("status", nextFilter); + } + }); + }; + + const setHistoryQuery = (nextQuery: string) => { + if (view !== "history") { + setLocalHistoryQuery(nextQuery); + return; + } + updateHistorySearchParams((params) => { + if (nextQuery.trim()) { + params.set("q", nextQuery); + } else { + params.delete("q"); + } + }); + }; + function handleDashTabKeyDown( event: ReactKeyboardEvent, currentIndex: number, diff --git a/apps/web/src/pages/SupervisionResearch.tsx b/apps/web/src/pages/SupervisionResearch.tsx index 876489b..8904407 100644 --- a/apps/web/src/pages/SupervisionResearch.tsx +++ b/apps/web/src/pages/SupervisionResearch.tsx @@ -78,6 +78,17 @@ function shortId(value: string | undefined, length = 12): string { return value.length > length ? `${value.slice(0, length)}…` : value; } +const LEGACY_TEACHER_SESSION_ROUTE = + /^\/teacher\/sessions\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\/?$/i; + +function normalizeDrilldownRoute(route: string): string { + if (route === "/teacher/dashboard") return "/teach"; + const legacySession = LEGACY_TEACHER_SESSION_ROUTE.exec(route); + return legacySession + ? `/teach/session/${legacySession[1]}/review` + : route; +} + function driftLabel(status: DriftStatus): string { if (status === "stable") return "안정"; if (status === "drift_flagged") return "드리프트 확인 필요"; @@ -217,7 +228,9 @@ function AttentionWorkbench({ items }: { items: AttentionItem[] }) { } const evidence = selected.evidence_pointer_ids.slice(0, 3); - const drilldowns = selected.drilldown_routes.slice(0, 3); + const drilldowns = selected.drilldown_routes + .slice(0, 3) + .map(normalizeDrilldownRoute); return (
diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index e54f17e..1f6af46 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -6,16 +6,13 @@ ## 현재 기준 -2026-09-08 Forgejo master `a0311c59` (배포 문서 포함 `d9bb5b36`)를 NAS `vignette-prod` 및 Cloudflare Pages에 정식 실배포했다. 사전 DB 덤프(`/volume1/docker/vignette-prod/backups/pre-deploy-a0311c59-.dump`, 12MB) 및 `env.nas` 백업을 생성한 뒤 NAS 컨테이너(API `sha256:8b1ed23d1d24...`, Web `sha256:5c34139d8b56...`)를 교체·기동했다. Cloudflare Pages는 174개 자산을 보존하며 `https://vignette.chanpaca.net`(미리보기: `2df910db.vignette-b1q.pages.dev`)으로 배포 완료했다. 공개 health(`https://api-vignette.chanpaca.net/health` status:ok, db/engine/openai:true), `/auth/me`(401 Unauthorized fail-closed), 웹 서비스 live 폐루프를 확인했다. +2026-09-09 최신 [개선관리 워크북](https://docs.google.com/spreadsheets/d/1MNRDfXoIQJbLzIcSRdHnQHREWELbKqV4/edit?pli=1&gid=1294900115#gid=1294900115)을 읽기 전용으로 재점검했다. 다운로드 원본 SHA-256 `f64bbc859354e8bfdd62bb3393cffd841ca3a0d865983967a149aea7cf569773`의 14개 항목은 완료 13·검토 1(C-001)이며 REQ-008~011도 원본 완료다. [항목별 구현 매핑](./ops/spreadsheet-implementation-audit-2026-09-09.md)은 원본 완료 표기, 현재 로컬 검증, 배포 상태를 분리한다. 원본 워크북은 수정하지 않았다. -TDD 기반 회귀 방지 및 핵심 불변식을 확정했다: -- REQ-010: 회기 무발화 0턴 분리 (`LearnerGrowthMetrics.ended_sessions` 및 대시보드 프로젝션에서 발화 없는 세션을 완료 회기 집계에서 안전하게 분리·제외) -- REQ-011: OAS G5 자기예측 `[0.0, 1.0]` 범위 도메인 검증 및 DB 트리거 수준 락 보장/예외 전환 (`CalibrationTransferStateError`) -- REQ-009: 웹 클라이언트 스트림 에러 시 입력문 복원(`setComposeText(text)`) 및 로딩 상태(`clientReplyPending`) 복원 보장 +REQ-010의 추가 결함은 0턴/client-only 회기가 대시보드의 첫 완료·반복·경험 배지 집계에 포함되는 것이었다. 기존 완료 집계와 같은 counselor/learner 발화 조건으로 정렬했고, 계약 RED 4건 뒤 GREEN 4건, 지정 API 20건과 API 전체 1108 passed / 1 skipped / 1 warning을 통과했다. `uc-session-conversation`과 `calibration-transfer` route fixture browser 재검증도 44 passed / 0 failed / 0 skipped로 수용했다. 이 browser 증거는 local fixture 기반이며 이번 변경은 build 미실행·새 public live 검증 미실행·미배포다. -단위/회귀 테스트는 API 전체 1104 passed / 1 skipped / 1 warning, gateway 전체 71 passed / 1 warning, web typecheck 및 build 전건 통과를 달성했다. +2026-09-08 Forgejo master `a0311c59`(배포 문서 포함 `d9bb5b36`)의 NAS/Cloudflare Pages 배포, 사전 DB 덤프·컨테이너 교체·당시 public health/auth/web 폐루프와 API 1104 passed 기록은 [배포 파이프라인](./ops/deployment-pipeline.md)과 [1차 배포 증거](./ops/evidence/nas-prod-deploy-2026-09-01.json)에 보존한다. 이는 오늘의 새 live 검증이나 현재 작업트리 배포 상태가 아니다. -증거: [NAS 배포 파이프라인](./ops/deployment-pipeline.md), [1차 배포 증거](./ops/evidence/nas-prod-deploy-2026-09-01.json). +실배포 CUA 128 시나리오는 카탈로그 생성만 끝났고 전체 검증은 끝나지 않았다. H09 기록 검색·필터 복귀와 T18 감독·연구 drilldown은 배포에서 RED다. H09는 local 16 passed, T18은 API 12 passed·local 교수자 15 passed로 수정 검증했지만 배포 재검증이 필요하다. 관리자와 일반 학습자 로그인에서 학습자의 `/admin`·`/teach`→`/learn` 차단과 P4 추천 일치를 확인했다. 순수 teacher, 독립 관찰자 공개 검증, OFF 계정, 교수자 쓰기와 모바일 실행은 남아 있다. 상세는 [실배포 CUA 보고](./qa/production-computer-use-report-2026-09-09.md)를 따른다. ## 남은 실행 항목 @@ -24,7 +21,8 @@ TDD 기반 회귀 방지 및 핵심 불변식을 확정했다: 3. 백업 운영화: 실패 알림, off-host 암호화 복제, 정기 restore drill을 운영 증거로 남긴다. 4. 아바타 fail-closed 후보: 원본과 DB 참조를 보존한 채 public fallback·digest receipt·브라우저 검증을 마친 뒤에만 승격 판단한다. 5. G7/G8 외부 게이트: G7은 동의 기반 물리 마이크와 독립 voice-gain 증거, G8은 실DB append-only 사람 게이트와 public proof가 남아 있다. -6. 신규 제품 이슈: REQ-009, REQ-010, REQ-011은 TDD 불변식 추가 및 헤드풀 브라우저 E2E 검증, 실서버 배포를 완료했다. 외부 원본 워크북과의 최종 동기화만 남아 있다. +6. REQ-009~011: 원본은 모두 완료다. API와 route fixture browser 로컬 재검증은 수용했지만 build·새 public live 검증·배포는 수행하지 않았다. +7. CUA-128: 허용된 관리자 P16 회기 1개와 학습자 P4 회기 1개를 사용해 총 2/2를 종료했다. P4 회기는 2분 19초·6턴·실제 AI 3왕복으로 종료했고, 기존 승인 범위의 대화·리뷰·워크시트 권한은 유지한다. 순수 teacher·OFF 계정, 교수자 쓰기와 모바일 실행이 남아 있다. 세부 ID·차단 이유·완료 조건은 [`TODO.md`](./TODO.md)와 [열린 백로그](./ops/backlog-2026-06-26.md)에 있다. @@ -35,11 +33,11 @@ TDD 기반 회귀 방지 및 핵심 불변식을 확정했다: 3. 외부 게이트는 증거 수집만 진행하며, 외부 입력·소유자 승인·비밀값을 추정하거나 생성하지 않는다. 4. 코드 또는 문서 변경 뒤에는 범위에 맞는 테스트와 SSOT checker를 실행한다. -2026-09-09 현재 baseline은 Python 3.11 API 전체 1104 passed/1 skipped/1 warning, gateway 전체 71 passed/1 warning이며 API 타입 체크와 web build도 통과했다. API의 1 skipped는 `VIGNETTE_USAGE_TEST_DATABASE_URL` 미설정 때만 건너뛰는 기존 선택형 격리 DB `test_admin_usage_postgres.py`이고 새 skip은 없다. +2026-09-09 REQ-010 API 재검증은 Python 3.11 API 전체 1108 passed/1 skipped/1 warning이다. API의 1 skipped는 `VIGNETTE_USAGE_TEST_DATABASE_URL` 미설정 때만 건너뛰는 기존 선택형 격리 DB `test_admin_usage_postgres.py`이고 새 skip은 없다. `uc-session-conversation`과 `calibration-transfer`의 route fixture browser 재검증은 44 passed/0 failed/0 skipped다. 이 수치는 local fixture 증거이며 build·새 public live 검증·배포를 뜻하지 않는다. 최종 UI 회귀는 `session-mvp`+`uc-session-coach-voice` 58 passed/0 skipped(exit 0, 2.6분), alliance+admin 39 passed/1 skipped(exit 0, 1.7분)로 합계 97 passed/1 skipped다. skip은 `uc-review-alliance-pulse` 320px 테스트의 desktop 중복 실행 제외이며 mobile은 실행·통과했다. `session-review`의 disabled AI feedback privacy 제어 focused 1 passed(exit 0, 4.4초)도 유지한다. dashboard 10은 별도 검증이다. -같은 날 source-only로 `test_session_turn_persistence.py`, `test_calibration_transfer_store.py`, `test_session_continuity_guard.py`는 78 passed와 `PendingDeprecationWarning` 1건, web typecheck는 통과했다. 이는 REQ-009~011의 재현·원인 해결·운영 증거가 아니다. 신규 제품 이슈의 현재 열린 상태와 다음 행동은 위 목록 및 [`TODO.md`](./TODO.md)를 따른다. +같은 날 source-only로 `test_session_turn_persistence.py`, `test_calibration_transfer_store.py`, `test_session_continuity_guard.py`는 78 passed와 `PendingDeprecationWarning` 1건이었다. 이는 당시 증거이며 REQ-009~011의 현 browser 재검증이나 운영 증거가 아니다. 신규 제품 이슈의 현재 열린 상태와 다음 행동은 위 목록 및 [`TODO.md`](./TODO.md)를 따른다. ## 승인과 금지 경계 diff --git a/docs/README.md b/docs/README.md index 288f6b7..9b43e3a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -108,6 +108,8 @@ | [`ops/evidence/avatar-decode-audit-2026-08-29.json`](./ops/evidence/avatar-decode-audit-2026-08-29.json) | 아바타 source union 93개·52,973 bytes·inventory SHA와 실제 decode 3 정상/90 실패, 현 DB 참조 2 정상/6 실패를 비식별 집계로 고정한 증거 | | [`ops/evidence/avatar-image-resilience-recovery-2026-08-29.json`](./ops/evidence/avatar-image-resilience-recovery-2026-08-29.json) | 공개 아바타 404 원인·시점 복구·인증 브라우저 DOM과 공통 이미지 fallback 후보의 privacy-safe 증거 | | [`ops/evidence/req008-reboot-preflight-2026-08-29.json`](./ops/evidence/req008-reboot-preflight-2026-08-29.json) | REQ-008 실제 재부팅 전 read-only task/runtime 사전점검 스냅샷 — 현재 public outage와 실제 reboot smoke가 이를 대체·완료하지 않음 | +| [`ops/spreadsheet-implementation-audit-2026-09-09.md`](./ops/spreadsheet-implementation-audit-2026-09-09.md) | 최신 개선관리 원본 14개 항목의 완료 13·검토 1(C-001)과 로컬 재검증·미배포 경계 | +| [`qa/production-computer-use-report-2026-09-09.md`](./qa/production-computer-use-report-2026-09-09.md) | 실제 배포 CUA 128 카탈로그·ledger, H09/T18 local 수정과 배포 재검증 경계 | | [`ops/evidence/professor-summary-gap-deploy-2026-08-29.json`](./ops/evidence/professor-summary-gap-deploy-2026-08-29.json) | 교수자 검토 큐·요약 카드 12px 분리의 RED→GREEN·7폭/전체 시각 게이트·clean Pages 배포·실서비스 DOM 증거 | | [`ops/public-db-recovery-rehearsal-2026-08-07.md`](./ops/public-db-recovery-rehearsal-2026-08-07.md) | 공개 DB 데이터 교체 사고의 RLS/volume 포렌식, 이중 dump, 격리 복구·최신 migration/API 리허설과 승인형 cutover/rollback 게이트 | | [`ops/server-migration-runbook-draft.md`](./ops/server-migration-runbook-draft.md) | 서버 이관 런북 초안(자택→학교 정보처) — 이식성 이슈·DB dump/restore·엔진 모드 전환·검증 (초안) | diff --git a/docs/TODO.md b/docs/TODO.md index 029d286..898a479 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -5,14 +5,14 @@ ## 우선 접수 제품 이슈 -> 원본: [개선관리 워크북](https://docs.google.com/spreadsheets/d/1MNRDfXoIQJbLzIcSRdHnQHREWELbKqV4/edit?gid=351278076#gid=351278076) `개발·기능 이슈` 2026-09-07 수집본, SHA-256 `625281e72067ae2aea402f2fe932b4f593b7905c35f8a9740cf346c7f077c812`. 외부 원본 14건과 로컬 집계(완료 9·기존 gate 2·신규 3)를 분리한다. 원본의 기존 대기와 로컬 DONE 불일치는 동기화 대기이며 원본을 수정하지 않는다. +> 최신 원본: [개선관리 워크북](https://docs.google.com/spreadsheets/d/1MNRDfXoIQJbLzIcSRdHnQHREWELbKqV4/edit?pli=1&gid=1294900115#gid=1294900115) `개발·기능 이슈`와 `임상·교육 이슈`를 2026-09-09에 읽기 전용으로 재점검했다. 다운로드 원본 SHA-256은 `f64bbc859354e8bfdd62bb3393cffd841ca3a0d865983967a149aea7cf569773`이며, 14개 항목은 완료 13·검토 1(C-001)이다. REQ-008~011도 원본에서는 완료다. [14개 항목 매핑](./ops/spreadsheet-implementation-audit-2026-09-09.md)은 원본 상태와 로컬 재검증을 구분한다. 원본을 수정하지 않는다. | ID | 로컬 상태 | 우선 행동 및 조치 결과 | 원본 상태 | 완료 조건 및 검증 증거 | |---|---|---|---|---| -| REQ-009 | DONE · E2E 검증완료 | 스트림 오류 시 인라인 재시도/닫기 버튼 추가, voiceStatus 복원, 부분 턴 미적재 보호 구현 완료. | 중요 · 대기 | 단위/Playwright 통과 및 실서버 헤드풀 브라우저 검증. [headful-02](./ops/evidence/headful-02-session-multi-turn.png) | -| REQ-011 | DONE · E2E 검증완료 | OAS G5 자기예측 시 legacy instrument ID 정규화 및 scoped block ID 일치화로 HTTP 422 해소. 1차 예측 저장 및 잠금 완료. | 일반 · 상태 공란 | 헤드풀 브라우저 리뷰 피드백 탭에서 1차 예측 저장 및 잠금 정상 동작 검증 완료. [headful-04](./ops/evidence/headful-04-review-calibration-locked.png) | -| REQ-010 | DONE · E2E 검증완료 | hard delete 없이 0턴 무턴 세션은 완료 회기 집계에서 안전하게 제외하고, 실제 발화 진행 회기만 완료 카운트에 반영. | 중요 · 상태 공란 | 헤드풀 브라우저 2턴 완료 시 카운트 증가(+1) 및 0턴 즉시 종료 시 카운트 불변(유지) 검증. [headful-05](./ops/evidence/headful-05-learner-home-after-completed.png), [headful-06](./ops/evidence/headful-06-learner-home-zero-turn-verified.png) | -| SRC-001 | DONE · 로컬 동기화완료 | 개선관리 워크북 replica의 수식 오류(`#REF!`) 수정 및 개발 상태 집계 범위(15행까지 포함) 보정 완료. | 원본 정합성 OPEN | 수식 수정본 엑셀 생성: `outputs/01a04217-f73b-7303-b597-401fa7f5d290/Vignette_개선관리_완료.xlsx` | +| REQ-009 | 원본 완료 · 기존 구현 재검증 수용 | 스트림 오류의 입력 복원·부분 턴 미저장·재시도/닫기·음성 상태 복원 구현을 route fixture 기반 browser 재검증으로 수용했다. | 완료 | `uc-session-conversation`과 `calibration-transfer` browser 44 passed/0 failed/0 skipped. fixture 기반 local proof이며 이번 재검증은 build·새 public live 검증·새 배포를 수행하지 않았다. | +| REQ-010 | 원본 완료 · 추가 수정 로컬 검증 수용 · 추가 수정 미배포 | 0턴/client-only 회기가 첫 완료·반복·경험 배지 집계에 섞이는 결함을 찾아 기존 완료 집계와 같은 counselor/learner 발화 조건으로 정렬했다. | 완료 | 계약 RED 4건 뒤 GREEN 4건, 지정 API 20건, API 전체 1108 passed/1 skipped/1 warning, browser 44 passed/0 failed/0 skipped. 이번 추가 수정은 build·새 public live 검증·배포를 수행하지 않았다. | +| REQ-011 | 원본 완료 · 기존 구현 재검증 수용 | OAS G5 자기예측 저장·잠금의 기존 해결 증거를 route fixture 기반 browser 재검증으로 수용했다. | 완료 | `uc-session-conversation`과 `calibration-transfer` browser 44 passed/0 failed/0 skipped. fixture 기반 local proof이며 이번 재검증은 build·새 public live 검증·새 배포를 수행하지 않았다. | +| SRC-001 | 역사 replica 수식 보정 완료 · 과거 수식 오류 미재현 | 과거 replica의 `#REF!` 및 15행 집계 범위 보정은 보존한다. 최신 원본 재점검에서는 이를 현재 원본 결함으로 주장하지 않는다. | 해당 없음 | 과거 수정본은 `outputs/01a04217-f73b-7303-b597-401fa7f5d290/Vignette_개선관리_완료.xlsx`에 보존한다. | ## P0 운영·외부 게이트 @@ -52,6 +52,7 @@ | MIGRATION-001 | idempotent DB migration runner 도입 여부를 결정한다. | init SQL과 runner의 역할 경계, 인프라 영향, 소유자 승인을 확정한다. | | OPS-001 | 운영 티켓 자동 분류·처리 후속을 설계한다. | 관리자 승인 뒤 이슈/PR/작업 스레드를 생성하는 범위와 audit 경계를 확정한다. | | E2E-001 | 전 기능 E2E 후속을 분류한다. | 이번 focused 범위에서 locator 18건을 교정하고 keyboard 1건을 해결했다. `npm run e2e` 전 기능 묶음에서 focused 범위 밖 케이스를 실행하고 실패별 재현·원인·수정 명령을 분류한다. 과거 sweep 실패를 오늘 재현 결함으로 단정하지 않으며, 실API/DB·장치 검증은 fixture 결과와 분리한다. | +| CUA-128 | 실제 배포 Computer Use 128 시나리오를 역할·승인 경계 안에서 재개한다. | 카탈로그 생성은 완료했으나 실배포 전체 검증은 미완료다. 일반 학습자 로그인·교차 역할 차단·P4 추천과 허용된 관리자 P16 1개·학습자 P4 1개 회기 종료를 확인했지만 H09·T18은 배포 재검증이 열려 있다. 순수 teacher·OFF 계정, 교수자 쓰기, 모바일과 독립 관찰자 공개 검증이 남아 있다. [실행 보고](./qa/production-computer-use-report-2026-09-09.md)를 따른다. | ## 임상 콘텐츠 후속 @@ -67,9 +68,9 @@ | 범위 | 상태 | 근거 | |---|---|---| -| C-002·C-003·REQ-001~007 | 완료 | 개선관리 완료본은 9 완료·2 검토를 유지한다. C-001과 REQ-008은 별도 게이트다. | +| 원본 14개 항목 | 원본 완료 13 · 검토 1 | 2026-09-09 최신 원본에서 REQ-001~011과 C-002·C-003은 완료, C-001은 검토다. 과거 완료 9·검토 2는 당시 11개 항목 수집본의 역사 기록이다. | | NAS A 경로 | 2026-09-01 당시 완료 | NAS `vignette-prod` 배포, migration 20/21/22, OAuth 실계정 새 사례→턴→리뷰 E2E, 첫 일일 backup sidecar 실행. | -| NAS 2차 실배포 & Cloudflare Pages | 2026-09-08 실배포 완료 | Forgejo/GitHub master `a0311c59`/`d9bb5b36` 배포 완료. NAS API·Web 컨테이너 교체, 사전 DB 덤프(12MB) 생성, Cloudflare Pages 174개 에셋 보존 배포. 실 health/auth/web 폐루프 검증. TDD 1104 passed (0턴 세션 분리, OAS calibration bounds & lock, composer 복원) 완료. [배포 파이프라인](./ops/deployment-pipeline.md) 참조. | +| NAS 2차 실배포 & Cloudflare Pages | 2026-09-08 당시 배포 기록 | Forgejo/GitHub master `a0311c59`/`d9bb5b36`의 당시 NAS API·Web 컨테이너 교체, 사전 DB 덤프(12MB), Cloudflare Pages 174개 에셋과 당시 health/auth/web 폐루프·TDD 1104 passed 기록을 보존한다. 이는 2026-09-09의 새 live 검증이나 현재 작업트리 배포 상태 주장이 아니다. [배포 파이프라인](./ops/deployment-pipeline.md) 참조. | | G0~G6·G8 내부 구현 | 완료 증거 보존 | G7 외부 종료와 G8 실DB/public 사람 게이트는 계속 열린다. | | B3·B3-신규 소유자 결정 | 결정 및 기존 구현 완료 | 원문은 [정리 전 백로그 원문](./archive/project-cleanup-2026-09-07/backlog-2026-06-26-before-cleanup.md), 남은 후속만 위에 둔다. | | 과거 Windows Compose 패키징 gate | SUPERSEDED | 9/1 NAS docker-compose 배포로 현 production 전환 항목에서 제거했다. 새 secret 주입 과제를 만들지 않는다. | diff --git a/docs/dev_dashboard.html b/docs/dev_dashboard.html index f061cb2..d126602 100644 --- a/docs/dev_dashboard.html +++ b/docs/dev_dashboard.html @@ -291,7 +291,7 @@
Vignette 커맨드센터팀장 리뷰 · dev status
내 차례 0 - 2026-09-01 KST + 2026-09-09 KST
@@ -300,9 +300,10 @@
project pulse -

지금 위치: NAS vignette-prod 정식 배포 완료 기록(SHA dce85620) — 2026-09-01 당시 배포·실계정 회기 검증·데이터 보존 증거를 남겼다. 신규 REQ-009~011 접수와 C-001·REQ-008 외부 게이트가 남았다.

+

지금 위치: 개선관리 원본 14개 재점검 — 2026-09-09 읽기 전용 확인에서 완료 13·검토 1(C-001)이다. REQ-009~011의 로컬 재검증은 수용했고, REQ-010 추가 수정은 미배포다. 2026-09-01·09-08 배포는 당시 증거이며 오늘의 새 live 검증이 아니다.

+

실배포 CUA 128: 카탈로그는 생성됐지만 전체 검증은 미완료다. 실제 배포에서는 H09(기록 검색·필터 복귀)와 T18(감독·연구 drilldown)이 RED로 남아 있다. 두 항목은 각각 local 16 passed와 API 12 passed·local 교수자 15 passed로 수정 검증했으나, 배포 재검증이 열려 있다. 관리자와 일반 학습자 로그인에서 학습자의 `/admin`·`/teach`→`/learn` 차단, P4 추천 일치와 허용된 관리자 P16 1개·학습자 P4 1개 회기 종료를 확인했다. 순수 teacher·OFF 계정, 교수자 쓰기, 모바일과 독립 관찰자 공개 검증은 남아 있다.

2026-08-31 배포 파이프라인 재정립(역사 기록): git 관리·배포는 git.chanpaca.net(Forgejo) 중심으로 이관했고, github.com(origin)은 private 백업/미러로 유지한다. 배포 대상은 NAS Production(docker-compose.nas.yml, vignette-prod)이며 이 PC는 개발용이다. Cloudflare tunnel은 NAS ingress의 현재 경로이므로 대체 ingress 검증 전에는 제거하지 않는다. 문서 deployment-pipeline.md.

-

최근 확인된 배포 증거(2026-09-01 당시): Forgejo master dce85620를 NAS에서 직접 clone·빌드해 vignette-prod api/engine/web을 교체했고, Cloudflare Pages production도 새 빌드(index-BFKGXJQi.js, 이전 세대 자산 보존)로 배포했다. 사전 dump cab6826b…(11.0MB·TOC 1783)·env 백업·구 이미지 보존으로 롤백 경로를 고정했다. 중복 활성 회기 14쌍 30행은 최신 유지 정책으로 종료(영수증 보존·삭제 0)하고 마이그레이션 20/21/22를 온라인 적용했다. 이 과정에서 mig22의 name[]=text[] 캐스트 결함과 master의 게이트웨이 openai provider 누락 회귀를 발견해 즉시 교정했다(테스트 71 passed). 배포 당시 public health ok·db/engine true, OpenAPI 200, /auth/me 401, dev-login 404, 데이터 집계 전후 동일(app_user 1380/sessions 618/turns 1743)을 확인했다. 증거: nas-prod-deploy-2026-09-01.json. headful 실계정 폐루프도 당시 완료: NAS env의 OAuth secret 오류(invalid_client)를 교정한 뒤 소유자 실계정 로그인→기존 회기 20건 표시(데이터 보존)→새 사례 회기 생성(mig22 실증)→실턴 SSE·AI 응답→종료→리뷰 생성까지 GREEN(sessions 619·turns 1745·case_profile 566). 일일 검증형 DB 백업 sidecar 첫 덤프도 11.2MB로 성공했다. 다음: 신규 REQ-009~011 재현, C-001 외부 임상 검수, NAS 재부팅 자동복구 smoke, 백업 실패 알림·off-host 복제.

+

최근 확인된 배포 증거(2026-09-01 당시): Forgejo master dce85620를 NAS에서 직접 clone·빌드해 vignette-prod api/engine/web을 교체했고, Cloudflare Pages production도 새 빌드(index-BFKGXJQi.js, 이전 세대 자산 보존)로 배포했다. 사전 dump cab6826b…(11.0MB·TOC 1783)·env 백업·구 이미지 보존으로 롤백 경로를 고정했다. 중복 활성 회기 14쌍 30행은 최신 유지 정책으로 종료(영수증 보존·삭제 0)하고 마이그레이션 20/21/22를 온라인 적용했다. 이 과정에서 mig22의 name[]=text[] 캐스트 결함과 master의 게이트웨이 openai provider 누락 회귀를 발견해 즉시 교정했다(테스트 71 passed). 배포 당시 public health ok·db/engine true, OpenAPI 200, /auth/me 401, dev-login 404, 데이터 집계 전후 동일(app_user 1380/sessions 618/turns 1743)을 확인했다. 증거: nas-prod-deploy-2026-09-01.json. headful 실계정 폐루프도 당시 완료: NAS env의 OAuth secret 오류(invalid_client)를 교정한 뒤 소유자 실계정 로그인→기존 회기 20건 표시(데이터 보존)→새 사례 회기 생성(mig22 실증)→실턴 SSE·AI 응답→종료→리뷰 생성까지 GREEN(sessions 619·turns 1745·case_profile 566). 일일 검증형 DB 백업 sidecar 첫 덤프도 11.2MB로 성공했다. 남은 운영·외부 게이트: C-001 외부 임상 검수, NAS 재부팅 자동복구 smoke, 백업 실패 알림·off-host 복제.

운영 복구·watchdog·배포 상세 보기

2026-08-31 사례 연속성 후보: source candidate에는 같은 NPC의 완전히 새로 시작(빈 새 case)과 이어서 진행(선택 case의 압축 기억)을 분리했고, 사례별 누적 회기·상담자 가시 턴·누적 시간을 시작 전 표시하며 기억은 lazy foldout으로 숨겼다. migration 22는 legacy case unique만 해제하고 migration 21의 단일 활성 회기 제약을 유지한다. 이는 source/test 후보 상태이며 NAS DB 적용·배포·실제 production 브라우저 GREEN은 아직 하지 않았다.

@@ -327,7 +328,7 @@ 이전 OpenAPI 증거 · Public API 126 paths · G1~G8 Direct-runtime preflight APP ROLE OK Avatar decode 3 valid · 90 invalid - Workbook 완료 9 · 검토 2 + Workbook 최신 원본 완료 13 · 검토 1 Human gate local 10/10 · IAB GREEN Human gate 실DB/public pending Decision P0 조건부 @@ -342,13 +343,13 @@
-

제품 이슈 접수 현황 (2026-09-07)

-

지표 범위: 이 표는 개선관리 워크북의 14개 항목을 로컬 추적용으로 분리한 것이다. 상단의 모듈 카드 33 done / 2 doing과 같은 지표가 아니다. 외부 원본은 개선관리 워크북이며 2026-09-07 수집 SHA-256 625281e72067ae2aea402f2fe932b4f593b7905c35f8a9740cf346c7f077c812다. 로컬 집계는 완료 9·기존 gate 2·신규 3이다. 원본의 기존 대기 표기와 로컬 DONE은 동기화 대기 상태이며 원본을 수정하지 않는다.

+

제품 이슈 원본 재점검 (2026-09-09)

+

지표 범위: 이 표는 개선관리 워크북 14개 항목의 최신 원본과 로컬 재검증을 분리한다. 다운로드 원본 SHA-256은 f64bbc859354e8bfdd62bb3393cffd841ca3a0d865983967a149aea7cf569773, 원본은 완료 13·검토 1(C-001)이며 REQ-008~011도 완료다. API 전체 1108 passed/기존 skip 1, route fixture browser 44 passed/0 failed/0 skipped를 수용했다. 이는 local proof이며 REQ-010 추가 수정은 미배포, 새 public live 검증은 하지 않았다.

- - - - + + + +
ID로컬 상태원본 상태로컬 조치 및 검증 결과검증 증거
REQ-009DONE · E2E 검증완료중요 · 대기상담 중 엔진 스트림 끊김/오류 시 입력 복원, 부분 턴 미적재 보호, 인라인 재시도/닫기 버튼 및 음성 상태 복원 구현 완료.단위·Playwright 통과 및 실서버 헤드풀 브라우저 검증. headful-02-session-multi-turn.png
REQ-010DONE · E2E 검증완료중요 · 상태 공란하드 딜리트 없이 0턴 무턴 세션은 완료 회기 집계에서 안전하게 제외하고, 실제 발화가 진행된 회기만 완료 카운트에 반영되도록 집계 프로젝션 교정.헤드풀 브라우저 2턴 완료 시 카운트 증가(+1) 및 0턴 즉시 종료 시 카운트 불변(유지) 검증. headful-05, headful-06
REQ-011DONE · E2E 검증완료일반 · 상태 공란OAS G5 자기예측 시 legacy instrument ID 정규화 및 scoped block ID 일치화로 HTTP 422 위반 오류 해소. 1차 예측 기록 및 잠금 성공.헤드풀 브라우저 리뷰 피드백 탭에서 1차 예측 저장 및 잠금 완료. headful-04-review-calibration-locked.png
SRC-001DONE · 로컬 동기화완료원본 워크북 정합성개선관리 워크북 replica의 수식 오류(#REF!) 수정 및 개발 상태 집계 범위(15행까지 포함) 보정 완료.수식 수정본 엑셀 생성: outputs/01a04217-f73b-7303-b597-401fa7f5d290/Vignette_개선관리_완료.xlsx
REQ-009원본 완료 · 기존 구현 재검증 수용완료입력 복원·부분 턴 미저장·재시도/닫기·음성 상태 복원을 local browser로 재검증했다.route fixture browser 44 passed/0 failed/0 skipped. 이번 재검증은 새 public live 검증·새 배포가 아니다.
REQ-010원본 완료 · 추가 수정 로컬 검증 수용완료0턴/client-only 회기를 첫 완료·반복·경험 배지 집계에서 기존 완료 조건과 같게 제외했다.API 1108 passed/기존 skip 1, route fixture browser 44 passed/0 failed/0 skipped. 추가 수정 미배포.
REQ-011원본 완료 · 기존 구현 재검증 수용완료자기예측 저장·잠금의 기존 해결을 local browser로 재검증했다.route fixture browser 44 passed/0 failed/0 skipped. 이번 재검증은 새 public live 검증·새 배포가 아니다.
SRC-001역사 replica 수식 보정해당 없음과거 replica의 #REF!·15행 집계 범위 보정은 보존하며 최신 원본에서 그 오류는 미재현이다.과거 수정본: outputs/01a04217-f73b-7303-b597-401fa7f5d290/Vignette_개선관리_완료.xlsx
@@ -1135,7 +1136,7 @@ Web typechecknpm run typecheckPassed Design SSOT / auth visualnpm run check:design-ssot / npx playwright test e2e/auth-visual.spec.ts --project=chromium-single-run --reporter=line / npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --reporter=lineSSOT checker passed; login/onboarding light-dark desktop-mobile 1 passed; 14 core screens × 7 widths visual gate 14 passed. - Full Playwright E2E baselinenpm run e2e:parallel / npm run e2e:single-run / npm run e2e:list2026-08-31 현재 수집은 1182 tests / 66 files다. 이 숫자는 수집량이며 현 작업트리 전체 GREEN과 동일하지 않다. G8 clean-head release gate는 candidate 112/112와 실제 NAS-origin 112/112를 통과했다. 이전 단일 120/120과 2026-07-15의 fixture desktop/mobile 166/166 + DB/engine/provider 직렬 49/49 = 215/215는 범위가 다른 역사 기준선으로 보존한다. + Full Playwright E2E baselinenpm run e2e:parallel / npm run e2e:single-run / npm run e2e:list2026-09-09 현재 수집은 1188 tests / 66 files다. 이 숫자는 수집량이며 현 작업트리 전체 GREEN과 동일하지 않다. G8 clean-head release gate는 candidate 112/112와 실제 NAS-origin 112/112를 통과했다. 이전 단일 120/120과 2026-07-15의 fixture desktop/mobile 166/166 + DB/engine/provider 직렬 49/49 = 215/215는 범위가 다른 역사 기준선으로 보존한다. Refactor governance P1~P8ruff check app / pytest -q app / pytest -q engine_gateway / npm run typecheck / npm run check:api-types / npm run check:design-ssot / npm run check:dead-code / npm run check:duplication / npm run build / npm audit --audit-level=high / full PlaywrightBackend 400 passed, gateway 29 passed, web gates/build/audit passed, vulnerabilities 0, production duplication 1 clone/15 lines/0.03%, Playwright 215/215 passed. 상세 근거는 archive/project-cleanup-2026-09-07/refactor-governance-2026-07-15.md. API typegen SSOTnpm run check:api-typesPassed; FastAPI OpenAPI → src/lib/api.gen.ts stale check Outcome & Alliance OS G0py -3.11 -X utf8 -m pytest -p no:cacheprovider apps/api/app/test_measurement_contract.py apps/api/app/test_runtime_schema_ssot.py -q / scripts/check-measurement-ledger.sql / measurement·API contract checks / web typecheck / DB-backed session-persistence focused E2E 3종G0 contract/schema 11 passed, 기존 backend 100 passed, auth 39 passed. Python→JSON Schema→TypeScript→PostgreSQL enum·필수필드 계약이 일치하고 8개 deterministic benchmark가 검증됐다. Live PostgreSQL에서 learner/client/evaluator 가시 행 1/1/2, 교차 누수 0, append-only guard 2를 확인했다. 학습자 턴→교수자 대시보드, 워크시트 검수, 종료 deep 평가→durable 리뷰 E2E는 각각 1 passed. G0/AOS-001~004 완료. diff --git a/docs/guides/testing.md b/docs/guides/testing.md index 277b2ee..7d34847 100644 --- a/docs/guides/testing.md +++ b/docs/guides/testing.md @@ -20,7 +20,7 @@ Vignette 저장소의 모든 검증 수단(백엔드 단위 테스트, 웹 타 | API 타입 생성 체크 | `apps/web` | `npm run check:api-types` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass | | 웹 타입체크 | `apps/web` | `npm run typecheck` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass | | 웹 빌드 | `apps/web` | `npm run build` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass | -| Playwright E2E(전체) | `apps/web` | `npm run e2e` | **필요(+시드)** | **필요** | 자동기동 | 일부만 | **필요** | 현재 수집 1182 tests / 66 files · 현 작업트리 전체 GREEN 미검증 | +| Playwright E2E(전체) | `apps/web` | `npm run e2e` | **필요(+시드)** | **필요** | 자동기동 | 일부만 | **필요** | 현재 수집 1188 tests / 66 files · 현 작업트리 전체 GREEN 미검증 | 핵심 원칙: **단위 테스트(pytest)와 타입체크/빌드는 외부 서비스 없이 단독 실행된다.** **E2E만 풀스택(DB+API+웹+브라우저)을 요구한다.** 아래 각 절에서 근거와 절차를 설명한다. @@ -86,6 +86,10 @@ py -3.11 -m pytest engine_gateway/ --collect-only -q #### 개선관리 워크북 focused 증거 (2026-08-27~28) +2026-09-09 최신 원본 워크북(다운로드 SHA-256 `f64bbc859354e8bfdd62bb3393cffd841ca3a0d865983967a149aea7cf569773`)은 14개 항목 중 완료 13·검토 1(C-001)이며 REQ-008~011도 완료다. `py -3.11 -m pytest app/ -q`는 1108 passed/기존 skip 1/1 warning, `uc-session-conversation`·`calibration-transfer` route fixture browser는 44 passed/0 failed/0 skipped다. 이는 local proof이며 REQ-010 추가 수정은 미배포다. 이 워크북 감사 단계에서는 build·새 public live 검증을 수행하지 않았다. 상세 매핑은 [`spreadsheet-implementation-audit-2026-09-09.md`](../ops/spreadsheet-implementation-audit-2026-09-09.md)를 따른다. + +실배포 CUA 128은 전체 완료가 아니다. H09의 `/learn/history` 검색·필터 복귀는 `uc-learner-practice-history` local 16 passed, T18의 감독·연구 canonical/legacy drilldown은 API 12 passed와 `uc-teacher-console` local 15 passed로 수정 검증했다. 두 RED는 배포 재검증 전까지 열린 상태이며 실제 배포 관찰은 [CUA 보고](../qa/production-computer-use-report-2026-09-09.md)와 ledger를 따른다. + 아래는 2026-09-07 전체 API 1090 passed/1 skipped/1 warning·gateway 71 passed/1 warning와 별도로 해당 계약을 좁혀 실행한 확정 증거다. 같은 묶음의 테스트가 여러 요구사항 경계를 함께 검증할 수 있으므로 숫자를 요구사항별 전체 합계로 더하지 않는다. @@ -317,7 +321,7 @@ VITE_API_BASE=http://127.0.0.1:8000 npm run e2e # 프록시 대신 API ### 3.6 실측 테스트 개수 (현재) -2026-08-31 `npx playwright test --list` 기준 **현재 수집 1182 tests / 66 files**다 +2026-09-09 `npx playwright test --list` 기준 **현재 수집 1188 tests / 66 files**다 (유스케이스 16테마 `uc-*.spec.ts` 239 시나리오 포함). 이 숫자는 수집량이지 통과량이 아니다. 현 작업트리 전체 1109개 완주는 아직 증거가 없으며, 과거 전체 GREEN 기록과 이번 focused/release gate 결과를 구분해 적는다. diff --git a/docs/ops/backlog-2026-06-26.md b/docs/ops/backlog-2026-06-26.md index e12a14f..9f5dcd6 100644 --- a/docs/ops/backlog-2026-06-26.md +++ b/docs/ops/backlog-2026-06-26.md @@ -4,16 +4,16 @@ > 2026-09-07 이전 완료 기록과 장문 이력은 [정리 전 백로그 원문](../archive/project-cleanup-2026-09-07/backlog-2026-06-26-before-cleanup.md)에 보존했다. > 2026-09-01 NAS 배포는 당시 [배포 증거](./evidence/nas-prod-deploy-2026-09-01.json)로 완료 기록을 남겼으며, 현재 live 상태 주장이나 후속 게이트 완료 선언이 아니다. -## 신규 제품 이슈 — 워크북 원본 보존형 접수 +## 제품 이슈 — 최신 워크북 원본과 로컬 재검증 -> 원본: [개선관리 워크북](https://docs.google.com/spreadsheets/d/1MNRDfXoIQJbLzIcSRdHnQHREWELbKqV4/edit?gid=351278076#gid=351278076) `개발·기능 이슈` 행 13~15, 2026-09-07 수집 SHA-256 `625281e72067ae2aea402f2fe932b4f593b7905c35f8a9740cf346c7f077c812`. 원본 14건은 완료 9·기존 gate 2·신규 3으로 로컬 분류한다. 외부 원본의 기존 대기와 로컬 DONE 불일치는 동기화 대기이며, 원본 문서를 이 백로그 작업으로 수정하지 않는다. +> 최신 원본: [개선관리 워크북](https://docs.google.com/spreadsheets/d/1MNRDfXoIQJbLzIcSRdHnQHREWELbKqV4/edit?pli=1&gid=1294900115#gid=1294900115)를 2026-09-09 읽기 전용으로 재점검했다. 다운로드 원본 SHA-256 `f64bbc859354e8bfdd62bb3393cffd841ca3a0d865983967a149aea7cf569773`, 14개 항목은 완료 13·검토 1(C-001)이며 REQ-008~011도 완료다. [항목별 구현 매핑](./spreadsheet-implementation-audit-2026-09-09.md)에서 원본 상태와 로컬 재검증을 분리한다. 과거 9 완료·2 검토, 신규 3 대기, 원본 `#REF!` 주장은 당시 11개 항목 replica의 역사 기록이며 최신 원본 상태가 아니다. 원본은 수정하지 않는다. | ID | 원본 사실 | 조치 및 결과 | 완료 조건 | 검증 증거 | |---|---|---|---|---| -| REQ-009 | 중요·대기. 상담 중 “엔진 연결 - 내담자 응답을 생성하지 못했습니다. 입력 내용은 복원했습니다.” 뒤 진행 불가. | 스트림 중단 오류 시 입력 복원, 부분 턴 미저장 안전 보장, 인라인 재시도 및 닫기 버튼 추가, voiceStatus idle 복원 완료. | 입력 복원, 부분 턴 미저장, 동일 회기 재시도 성공 증명. | 단위/Playwright 통과 및 실서버 헤드풀 E2E 검증. [headful-02](./evidence/headful-02-session-multi-turn.png) | -| REQ-010 | 중요·상태 공란. 시작 직후 창닫기 회기가 완료 회기로 카운트될 우려와 잘못 생성한 미완료 회기의 삭제/재시작 요청. | 하드 딜리트 없이 0턴 세션은 `completed_sessions` 집계에서 안전하게 제외하고, 실제 발화 진행 회기만 완료 카운트에 반영. | 무턴 중단 제외, 정상 완료 반영, 기존 데이터 보존 검증. | 헤드풀 브라우저 2턴 완료 시 +1 및 0턴 즉시 종료 시 유지 검증 완료. [headful-05](./evidence/headful-05-learner-home-after-completed.png), [headful-06](./evidence/headful-06-learner-home-zero-turn-verified.png) | -| REQ-011 | 일반·상태 공란. 첫 자기예측 이유 입력 뒤 버튼 오류로 저장·리뷰 완료·다음 회기가 막힘. 워크북 M15의 [연구팀 첨부 수집본](./evidence/req011-prediction-error-2026-09-07.png)은 HTTP 422 `prediction revision violated provenance or history invariants`를 보인다. | OAS G5 자기예측 시 legacy instrument ID 정규화 및 scoped block ID 일치화로 HTTP 422 해소. 1차 예측 저장 및 잠금 완료. | 정상 저장, 중복 방지, 리뷰 진행·다음 회기 가능, 평가 공개 순서를 함께 증명. | 헤드풀 브라우저 리뷰 피드백 탭에서 1차 예측 저장 및 잠금 완료. [headful-04](./evidence/headful-04-review-calibration-locked.png) | -| SRC-001 | 원본 워크북 정합성 OPEN. `요약!B16:B19`의 `COUNTIF(#REF!)`, 개발 상태 집계 범위 `I5:I14`가 REQ-011 행 15를 놓침. | 개선관리 워크북 replica 수식 (#REF! 제거, 범위 `$I$5:$I$15`, `$B$5:$B$15`) 보정 완료. | 요약 수식·상태 집계가 14건을 반영하고 로컬 분류와 차이를 설명. | 수식 수정본 엑셀 생성: `outputs/01a04217-f73b-7303-b597-401fa7f5d290/Vignette_개선관리_완료.xlsx` | +| REQ-009 | 원본 완료. | 스트림 오류 입력 복원·부분 턴 미저장·재시도/닫기·음성 상태 복원을 local browser 재검증으로 수용했다. | 배포 상태 분리. | `uc-session-conversation`·`calibration-transfer` route fixture browser 44 passed/0 failed/0 skipped. build·새 public live 검증·배포는 수행하지 않았다. | +| REQ-010 | 원본 완료. | 0턴/client-only 회기가 첫 완료·반복·경험 배지에 포함되던 결함을 기존 완료 집계와 같은 counselor/learner 발화 조건으로 정렬했다. | 배포 상태 분리. | 계약 RED 4건→GREEN 4건, 지정 API 20건, API 전체 1108 passed/1 skipped/1 warning, route fixture browser 44 passed/0 failed/0 skipped. build·새 public live 검증·배포는 수행하지 않았다. | +| REQ-011 | 원본 완료. | 자기예측 저장·잠금을 local browser 재검증으로 수용했다. | 배포 상태 분리. | `uc-session-conversation`·`calibration-transfer` route fixture browser 44 passed/0 failed/0 skipped. build·새 public live 검증·배포는 수행하지 않았다. | +| SRC-001 | 역사 replica 수식 보정. | 과거 replica의 `#REF!`와 15행 집계 범위 보정은 보존한다. | 최신 원본 오류로 오인하지 않는다. | 과거 수정본: `outputs/01a04217-f73b-7303-b597-401fa7f5d290/Vignette_개선관리_완료.xlsx` | REQ-009 초기 검증 명령: @@ -24,7 +24,7 @@ cd ../web npx playwright test e2e/uc-session-conversation.spec.ts --grep '스트림 도중 오류|done 없이' --project=chromium-desktop --workers=1 ``` -2026-09-07 Python 3.11 source-only 묶음은 `test_session_turn_persistence.py`, `test_calibration_transfer_store.py`, `test_session_continuity_guard.py`에서 78 passed와 `PendingDeprecationWarning` 1건이었다. 이는 REQ-009~011의 재현·원인 해결·운영 증거가 아니다. +2026-09-07 Python 3.11 source-only 묶음은 `test_session_turn_persistence.py`, `test_calibration_transfer_store.py`, `test_session_continuity_guard.py`에서 78 passed와 `PendingDeprecationWarning` 1건이었다. 이는 당시 증거이며 REQ-009~011의 현 재검증이나 운영 증거가 아니다. ## P0 운영 게이트 @@ -64,10 +64,11 @@ npx playwright test e2e/uc-session-conversation.spec.ts --grep '스트림 도중 | MIGRATION-001 | idempotent DB migration runner | init SQL과 runner의 역할 경계·인프라 영향·소유자 승인 | | OPS-001 | 운영 티켓 자동 분류 | 관리자 승인·audit 경계를 갖춘 구현 패킷 | | E2E-001 | 전 기능 E2E 후속 분류 | focused 범위의 locator 18건 교정·keyboard 1건 해결 뒤 `npm run e2e`에서 범위 밖 케이스를 실행하고, 실패별 재현·원인·수정 명령을 분류한다. 과거 sweep 실패를 오늘 결함으로 단정하지 않으며 실API/DB·장치 검증은 fixture 결과와 분리한다. | +| CUA-128 | 실제 배포 Computer Use 재검증 | 128 시나리오 카탈로그 생성은 완료했지만 전체 실배포 검증은 미완료다. 일반 학습자 로그인·교차 역할 차단·P4 추천과 허용된 관리자 P16 1개·학습자 P4 1개 회기 종료를 확인했으나 H09·T18은 local 수정 증거 뒤 배포 재검증이 열려 있다. 순수 teacher·OFF 계정, 교수자 쓰기, 모바일과 독립 관찰자 공개 검증이 남아 있다. | ## 완료 기록 경계 -2026-09-01 A 경로의 NAS 배포, migration 20/21/22 적용, OAuth 실계정 회기 E2E와 당시 backup sidecar 첫 실행은 완료 이력이다. 2026-09-08에는 Forgejo master `a0311c59`/`d9bb5b36`의 NAS Production 컨테이너 교체와 Cloudflare Pages 배포, 사전 DB 덤프(12MB) 확보 및 0턴 세션 분리·OAS calibration bounds/lock·composer 복원 등 TDD 회귀 검증(API 1104 passed)을 완료했다. B3 및 B3-신규의 소유자 결정과 완료된 구현도 archive snapshot에 보존한다. `CLEANUP-001`은 84 regular source와 supplementary 4 files·junction 4개의 대조 뒤 `outputs/local-history/retained-sources-2026-09-07/`으로 가역 보존 이동을 마쳤고 영구 삭제는 0건이다. `RUNTIME-001`은 Windows public-runtime/watchdog scripts를 복구 경로로 보존하기로 결정했으며 이 PC의 두 Disabled Scheduled Task는 운영 서버 다운을 뜻하지 않는다. `REFACTOR-001`은 `sessionViewModel` 분리와 `voiceCapture` 책임 유지를 수용했고, lifecycle setter bag hook은 채택하지 않았다. Space retry→pause와 empty-audio fixture를 교정했으며, `learnerFeedbackEnabled:true` 누락으로 숨던 review feedback UI는 fixture 보완 뒤 focused login→review 1 passed(exit 0)로 확인했다. 제품 feedback guard는 유지했다. `REFACTOR-002`는 repository와 persistence values로 평가 저장 경계를 분리했다. 열린 백로그에 완료 이력을 다시 적지 않는다. +2026-09-01 A 경로의 NAS 배포, migration 20/21/22 적용, OAuth 실계정 회기 E2E와 당시 backup sidecar 첫 실행은 완료 이력이다. 2026-09-08 Forgejo master `a0311c59`/`d9bb5b36`의 NAS Production 컨테이너 교체, Cloudflare Pages 배포, 사전 DB 덤프(12MB), 0턴 세션 분리·OAS calibration bounds/lock·composer 복원 TDD(API 1104 passed)는 당시 배포 증거다. 이는 2026-09-09의 새 live 검증이나 현재 작업트리의 배포 상태를 뜻하지 않는다. B3 및 B3-신규의 소유자 결정과 완료된 구현도 archive snapshot에 보존한다. `CLEANUP-001`은 84 regular source와 supplementary 4 files·junction 4개의 대조 뒤 `outputs/local-history/retained-sources-2026-09-07/`으로 가역 보존 이동을 마쳤고 영구 삭제는 0건이다. `RUNTIME-001`은 Windows public-runtime/watchdog scripts를 복구 경로로 보존하기로 결정했으며 이 PC의 두 Disabled Scheduled Task는 운영 서버 다운을 뜻하지 않는다. `REFACTOR-001`은 `sessionViewModel` 분리와 `voiceCapture` 책임 유지를 수용했고, lifecycle setter bag hook은 채택하지 않았다. Space retry→pause와 empty-audio fixture를 교정했으며, `learnerFeedbackEnabled:true` 누락으로 숨던 review feedback UI는 fixture 보완 뒤 focused login→review 1 passed(exit 0)로 확인했다. 제품 feedback guard는 유지했다. `REFACTOR-002`는 repository와 persistence values로 평가 저장 경계를 분리했다. 열린 백로그에 완료 이력을 다시 적지 않는다. ## 확인 명령 diff --git a/docs/ops/spreadsheet-implementation-audit-2026-09-09.md b/docs/ops/spreadsheet-implementation-audit-2026-09-09.md new file mode 100644 index 0000000..235e3ee --- /dev/null +++ b/docs/ops/spreadsheet-implementation-audit-2026-09-09.md @@ -0,0 +1,38 @@ +# 개선관리 워크북 구현 정합성 재점검 + +> 범위: 2026-09-09 KST에 원본 워크북을 읽기 전용으로 다시 확인하고, 원본의 완료 표기와 현재 작업트리의 검증 상태를 분리해 기록한다. 원본 워크북은 수정하지 않았다. + +## 원본 기준 + +- 원본: [개선관리 워크북](https://docs.google.com/spreadsheets/d/1MNRDfXoIQJbLzIcSRdHnQHREWELbKqV4/edit?pli=1&gid=1294900115#gid=1294900115) +- 다운로드 원본 SHA-256: `f64bbc859354e8bfdd62bb3393cffd841ca3a0d865983967a149aea7cf569773` +- 확인 범위: `개발·기능 이슈` A5:M15, `임상·교육 이슈` A5:M7, `요약` D7 및 14개 요구 항목 +- 원본 집계: 완료 13개, 검토 1개(C-001). REQ-001~011과 C-002·C-003은 완료이며 C-001만 검토다. +- 최신 원본에서 `#REF!` 수식 오류를 확인하지 못했다. 과거 11개 항목 replica의 `#REF!`/행 범위 보정 기록은 역사 증거로만 유지한다. + +## 요구사항 매핑 + +| 원본 ID | 원본 상태 | 코드 근거 | 구현·검증 판정 | +|---|---|---|---| +| REQ-001 | 완료 | `apps/api/app/routes/auth.py` | Google 로그인 제한 해제와 계정 복구의 기존 증거를 보존한다. 이번 재점검에서 새 live 검증은 수행하지 않았다. | +| REQ-002 | 완료 | `apps/api/app/routes/protocols.py` | 프로토콜 관리의 기존 focused·DB 증거를 보존한다. 이번 재점검에서 새 live 검증은 수행하지 않았다. | +| REQ-003 | 완료 | `apps/api/app/persona_read_model.py` | 페르소나 주호소 표시의 기존 DB/browser 증거를 보존한다. 이번 재점검에서 새 live 검증은 수행하지 않았다. | +| REQ-004 | 완료 | `apps/api/app/services/feedback_policy.py` | 계정별 AI 피드백 제어의 기존 정책·browser 증거를 보존한다. 이번 재점검에서 새 live 검증은 수행하지 않았다. | +| REQ-005 | 완료 | `apps/api/app/routes/sessions.py`, `apps/api/app/session_persistence.py` | 신규 페르소나 pin과 회기 생성의 기존 DB lifecycle 증거를 보존한다. 이번 재점검에서 새 live 검증은 수행하지 않았다. | +| REQ-006 | 완료 | `apps/api/app/session_evaluation_input.py` | 피드백 개인정보 비식별화의 기존 focused 증거를 보존한다. 이번 재점검에서 새 live 검증은 수행하지 않았다. | +| REQ-007 | 완료 | `apps/api/app/routes/sessions.py` | 종료 뒤 평가 생성·다음 회기 진행의 기존 persistence 증거를 보존한다. 이번 재점검에서 새 live 검증은 수행하지 않았다. | +| REQ-008 | 완료 | `apps/api/app/test_engine_health_contract.py` | 원본 기능 완료와 NAS 실제 재부팅 자동복구 smoke는 별개다. 후자는 여전히 소유자 승인 뒤 수행할 운영 게이트이며 이 원본 완료 표기로 닫지 않는다. | +| REQ-009 | 완료 | `apps/web/src/pages/Session.tsx` | 스트림 오류 입력 복원·재시도·부분 턴 미저장을 route fixture browser 44 passed로 재검증했다. 이번 재검증은 build·새 public live 검증·새 배포를 수행하지 않았다. | +| REQ-010 | 완료 | `apps/api/app/session_dashboard_projection.py` | 0턴/client-only 회기가 첫 완료·반복·경험 배지 집계에 포함되던 결함을 기존 완료 집계와 같은 counselor/learner 발화 조건으로 정렬했다. 계약 RED 4건 뒤 GREEN 4건, 지정 API 20건과 API 전체 1108 passed/1 skipped/1 warning, route fixture browser 44 passed를 통과했다. 이번 추가 수정은 build·새 public live 검증·배포를 수행하지 않았다. | +| REQ-011 | 완료 | `apps/api/app/services/calibration_transfer_store.py` | 자기예측 저장·잠금을 route fixture browser 44 passed로 재검증했다. 이번 재검증은 build·새 public live 검증·새 배포를 수행하지 않았다. | +| C-001 | 검토 | `apps/api/app/services/orchestrator.py` | 기술 사전검증과 외부 임상 승인은 다르다. 사례별 검수·청소년 답변·검토자 자격을 포함한 승인·서명 증거가 채워져 `review_complete=true`가 되기 전에는 검토 상태를 유지한다. | +| C-002 | 완료 | `apps/api/app/session_read_model.py` | 첫 회기 라포·개방 질문 점검의 기존 focused 증거를 보존한다. 이번 재점검에서 새 live 검증은 수행하지 않았다. | +| C-003 | 완료 | `apps/api/app/session_dashboard_projection.py` | 훈련 편향 지표의 기존 focused 증거를 보존한다. 이번 재점검에서 새 live 검증은 수행하지 않았다. | + +## 현재 로컬 검증과 남은 범위 + +REQ-010의 API 전체 재검증은 `py -3.11 -m pytest app/ -q`에서 1108 passed, 1 skipped, 1 warning으로 끝났다. 이 전체 범위에는 위 REQ-001~008과 C-001~003의 기존 백엔드 계약도 포함되며, 항목별 과거 focused/live 증거를 현재 live proof로 바꾸지는 않는다. skip은 기존 선택형 격리 DB 테스트이며 새 skip이 아니다. 로그는 `outputs/req-sheet-audit-2026-09-09/api-full-after.stdout.log`에 보존한다. + +browser 전체 재검증은 `npx playwright test e2e/uc-session-conversation.spec.ts e2e/calibration-transfer.spec.ts --workers=2`에서 44 passed, 0 failed, 0 skipped로 끝났다. 초기에 12건은 `session_id` query가 없는 exact fixture가 `me?session_id=...` 요청을 받지 못한 것이었고, 2건은 역할이 정해지지 않은 익명 인물의 기대값 drift였다. 제품 privacy guard나 마스킹 코드는 완화하지 않고 query route fixture 6건과 NAME 기대값만 바로잡았다. 이 증거는 route fixture 기반 local proof이며 build·새 public live 검증·배포를 뜻하지 않는다. + +2026-09-08 NAS/Pages 배포 증거는 당시 기록으로 보존한다. 이번 문서는 그 기록을 현재 live 검증으로 재사용하지 않으며, REQ-010의 이번 추가 수정은 미배포다. diff --git a/docs/qa/production-computer-use-128-2026-09-09.json b/docs/qa/production-computer-use-128-2026-09-09.json new file mode 100644 index 0000000..f19406e --- /dev/null +++ b/docs/qa/production-computer-use-128-2026-09-09.json @@ -0,0 +1,2175 @@ +{ + "schema_version": "1.1.0", + "deployed_origin": "https://vignette.chanpaca.net", + "observed_build": null, + "deployed_asset": "/assets/index-p7x09Xs2.js", + "deployed_commit": null, + "account_roles": { + "admin": "현재 로그인 확인됨", + "learner_test": "역할 유효성 검증에는 실제 learner_test 세션 필요", + "teacher_test": "역할 유효성 검증에는 실제 teacher_test 세션 필요" + }, + "current_execution_gate": "현재 로그인한 관리자 계정으로 검증 회기 2개와 비식별 가상 대화·리뷰·워크시트 저장이 허용됨. 역할 접근거부 검증은 해당 역할 계정으로 실행하며 관리자 전환을 PASS 대체로 쓰지 않음.", + "run_id": "production-cu-2026-09-09", + "method": "computer-use-real-backend", + "actor_counts": { + "admin_test": 19, + "authenticated_role": 3, + "learner_test": 80, + "teacher_test": 19, + "unauthenticated": 7 + }, + "side_effect_counts": { + "auth_session": 4, + "none": 106, + "test_data_mutation": 14, + "test_data_mutation_requires_owner_approval": 4 + }, + "execution_constraints": [ + "baseline 카탈로그의 모든 scenario는 NOT_RUN과 빈 evidence로 시작한다. 실제 결과는 실행 원장에서만 기록한다.", + "실행 때 사전조건(계정·테스트 데이터·권한·시간·자연 발생 오류)이 충족되지 않으면 그 실행 시도만 BLOCKED로 기록한다. baseline status를 BLOCKED로 바꾸지 않는다.", + "mock, page.route, localStorage 인증 조작, API 직접 write는 금지한다.", + "현재 로그인한 관리자 계정은 비식별 검증 회기 2개와 가상 대화·리뷰·워크시트 저장만 허용한다. 실제 사용자 데이터 수정, 운영 엔진 변경, 자료 공개, 승인 제출, 삭제는 실행하지 않는다.", + "role 유효성 및 접근거부는 요구된 실제 역할 계정으로 검증한다. admin 계정 또는 공간 전환은 learner_test 접근거부 PASS를 대체하지 않는다.", + "다른 화면폭 재실행은 같은 ID의 variant로 기록하며 scenario count를 늘리지 않는다.", + "source에서 확인되지 않은 UI·기능은 존재한다고 가정하지 않는다. 실제 화면에서 확인할 때까지 기능확인필요로 유지하고 PASS로 처리하지 않는다." + ], + "scenarios": [ + { + "id": "A01", + "actor": "unauthenticated", + "goal": "루트 비로그인 진입", + "preconditions": "비로그인 새 브라우저 프로필", + "steps": [ + "CUA의 새 비로그인 브라우저 프로필에서 `https://vignette.chanpaca.net/`를 연다.", + "주소가 `/login`으로 바뀔 때까지 기다린다.", + "`로그인` 제목과 `Google 계정으로 로그인` 버튼을 확인한다." + ], + "expected": "로그인 화면/Google CTA", + "side_effect": "none", + "required_data": "비로그인 브라우저 상태; 계정 데이터 불필요", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "A02", + "actor": "unauthenticated", + "goal": "학습홈 비로그인 직접주소", + "preconditions": "비로그인 새 브라우저 프로필", + "steps": [ + "비로그인 새 프로필에서 `https://vignette.chanpaca.net/learn`를 연다.", + "`/login`으로 이동한 뒤 로그인 패널의 `Google 계정으로 로그인` 버튼을 확인한다.", + "학습 대시보드·회기 기록·내담자 목록이 보이지 않는지 확인한다." + ], + "expected": "로그인으로 안전복귀", + "side_effect": "none", + "required_data": "비로그인 브라우저 상태; 계정 데이터 불필요", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "A03", + "actor": "unauthenticated", + "goal": "실제 존재 학습회기 deep link 비로그인", + "preconditions": "비로그인 새 브라우저 프로필; 실제 존재하는 테스트 회기 ID", + "steps": [ + "비로그인 새 프로필에서 실제 테스트 회기 ID를 넣은 `/learn/session/{테스트회기ID}`를 연다.", + "`/login`으로 이동한 뒤 `Google 계정으로 로그인` 버튼을 확인한다.", + "`실시간 상담 축어록`, `학습자 발화 입력`, 회기 내용이 로그인 전 화면에 없는지 확인한다." + ], + "expected": "내용 미노출 로그인", + "side_effect": "none", + "required_data": "실제 존재하는 테스트 회기 ID(비식별); 비로그인 새 브라우저 프로필", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "A04", + "actor": "unauthenticated", + "goal": "교수자콘솔 비로그인", + "preconditions": "비로그인 새 브라우저 프로필", + "steps": [ + "비로그인 새 프로필에서 `https://vignette.chanpaca.net/teach`를 연다.", + "`/login`으로 이동한 뒤 로그인 패널을 확인한다.", + "`교수자 검토 요약`, `학습자 검색`, 학습자 행이 로그인 전 화면에 없는지 확인한다." + ], + "expected": "학생정보 미노출 로그인", + "side_effect": "none", + "required_data": "비로그인 브라우저 상태; 계정 데이터 불필요", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "A05", + "actor": "unauthenticated", + "goal": "관리자 비로그인", + "preconditions": "비로그인 새 브라우저 프로필", + "steps": [ + "비로그인 새 프로필에서 `https://vignette.chanpaca.net/admin`를 연다.", + "`/login`으로 이동한 뒤 로그인 패널을 확인한다.", + "관리자 사이드바의 `운영 홈`, `AI 운영`, `사용자`, `티켓`이 로그인 전 화면에 없는지 확인한다." + ], + "expected": "운영정보 미노출 로그인", + "side_effect": "none", + "required_data": "비로그인 브라우저 상태; 계정 데이터 불필요", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "A06", + "actor": "unauthenticated", + "goal": "로그인 키보드만 OAuth 진입", + "preconditions": "비로그인 새 브라우저 프로필", + "steps": [ + "비로그인 새 프로필에서 `/login`을 연다.", + "Tab으로 `Google 계정으로 로그인` 버튼에 가시 포커스를 이동한다.", + "Enter를 눌러 `accounts.google.com`의 `/v3/signin/accountchooser` 인증 화면을 연다.", + "계정 선택·제출 전에 중지하고 host와 pathname, `계정을 선택하세요`만 증거로 남긴다." + ], + "expected": "포커스와 Google 인증화면 정상(계정선택 제출 전 중지 가능)", + "side_effect": "none", + "required_data": "비로그인 브라우저 상태; Google 계정 선택·제출 불필요", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "A07", + "actor": "learner_test", + "goal": "일반Google 테스트계정 로그인", + "preconditions": "해당 역할로 인증된 테스트 계정", + "steps": [ + "`/login`에서 `Google 계정으로 로그인`을 누른다.", + "소유자가 일반 Google 테스트 계정의 자격증명을 직접 입력하고 인증을 완료한다.", + "역할 홈(`/learn`, `/teach`, `/admin` 중 해당 경로)에서 사용자 이름과 역할 공간을 확인한다.", + "`승인된 이메일 도메인 또는 관리자 등록 계정만` 차단 문구가 표시되지 않는지 확인한다." + ], + "expected": "허용도메인차단없이 자기계정 진입", + "side_effect": "auth_session", + "required_data": "소유자가 직접 입력하는 일반 Google 테스트 계정", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "A08", + "actor": "unauthenticated", + "goal": "OAuth 취소후 재시도", + "preconditions": "비로그인 새 브라우저 프로필", + "steps": [ + "비로그인 상태에서 `/login`의 `Google 계정으로 로그인` 버튼을 누른다.", + "Google 계정 선택 화면에서 제출하지 않고 브라우저 뒤로가기로 `/login`에 돌아온다.", + "같은 `Google 계정으로 로그인` 버튼을 다시 활성화한다.", + "Google 인증 선택 화면이 다시 열리는지 확인한다." + ], + "expected": "로그인화면 복귀 후 재진입 가능", + "side_effect": "none", + "required_data": "비로그인 브라우저 상태; Google 계정 선택·제출 불필요", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "A09", + "actor": "authenticated_role", + "goal": "로그인된 사용자의 새탭 진입", + "preconditions": "비로그인 또는 해당 역할 인증 상태의 브라우저; 실제 사용자 데이터는 읽지 않음", + "steps": [ + "인증된 테스트 계정이 있는 브라우저에서 새 탭을 연다.", + "새 탭 주소창에 `https://vignette.chanpaca.net/`를 입력한다.", + "현재 계정의 역할에 맞는 `/learn`, `/teach`, 또는 `/admin` 경로와 역할용 사이드바를 확인한다." + ], + "expected": "역할에 맞는 자기 화면", + "side_effect": "none", + "required_data": "인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "A10", + "actor": "authenticated_role", + "goal": "인증상태 새로고침", + "preconditions": "해당 역할로 인증된 테스트 계정", + "steps": [ + "인증된 테스트 계정으로 역할 홈을 연다.", + "브라우저 새로고침을 한 번 실행한다.", + "`/login`으로 반복 이동하지 않고 같은 역할 홈과 사용자 표시가 다시 나타나는지 확인한다." + ], + "expected": "로그아웃루프없이 복원", + "side_effect": "none", + "required_data": "인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "A11", + "actor": "authenticated_role", + "goal": "잘못된 앱주소 복귀", + "preconditions": "기능확인필요", + "steps": [ + "인증 상태에 맞는 새 탭에서 `/e2e-not-found-20260909`를 연다.", + "라우터가 안전한 루트로 이동하는 동안 기다린다.", + "비로그인이면 `/login`과 `Google 계정으로 로그인`, 로그인 상태면 역할 홈을 확인해 앱 탐색을 재개한다." + ], + "expected": "404/안전한홈 이동으로 사용재개", + "side_effect": "none", + "required_data": "잘못된 경로 문자열; 계정 데이터 불필요", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "A12", + "actor": "learner_test", + "goal": "로그아웃후 뒤로가기", + "preconditions": "해당 역할로 인증된 테스트 계정", + "steps": [ + "소유자가 허용한 폐기 가능 학습자 테스트 계정으로 `/learn`을 연다.", + "톱바의 `aria-label=로그아웃` 버튼을 누르고 `/login` 이동을 확인한다.", + "브라우저 뒤로가기를 한 번 실행한다.", + "`실시간 상담 축어록`, `회기 기록`, `연습 페르소나` 같은 보호 내용이 다시 표시되지 않는지 확인한다." + ], + "expected": "보호된 내용 재노출없음(테스트계정만)", + "side_effect": "auth_session", + "required_data": "소유자가 허용한 폐기 가능 학습자 테스트 계정", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "C01", + "actor": "learner_test", + "goal": "학습홈에서 연습으로 이동", + "preconditions": "인증된 학습자 테스트 계정", + "steps": [ + "인증된 학습자 테스트 계정으로 `/learn`을 연다.", + "사이드바의 `학습` 링크를 선택한다.", + "주소가 `/learn/practice`인지와 `연습할 내담자를 선택합니다.` 제목을 확인한다.", + "`연습 페르소나` listbox에 실제 서버에서 받은 option 카드가 표시되는지 확인한다." + ], + "expected": "실서버 사례목록 표시", + "side_effect": "none", + "required_data": "인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "C02", + "actor": "learner_test", + "goal": "제공 사례 선택", + "preconditions": "인증된 학습자 테스트 계정", + "steps": [ + "`/learn/practice`의 `연습 페르소나` listbox에서 사용 가능한 option 카드 하나를 선택한다.", + "선택한 카드의 `aria-selected=true`와 체크 표시를 확인한다.", + "오른쪽 미리보기의 코드·이름이 선택 카드와 같은지 확인한다.", + "`주호소` 아래 요약이 선택 카드의 `presenting_summary`와 일치하는지 확인한다." + ], + "expected": "선택명/미리보기 일치", + "side_effect": "none", + "required_data": "인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "C03", + "actor": "learner_test", + "goal": "사례 상세 주호소 확인", + "preconditions": "인증된 학습자 테스트 계정", + "steps": [ + "`연습 페르소나`에서 사용 가능한 테스트 카드를 선택한다.", + "미리보기의 `주호소` 텍스트를 기록한다.", + "같은 미리보기에서 `난도`, `접근`, `음성` 값을 별도로 확인한다.", + "주호소 요약이 난도·접근 값과 섞여 한 필드로 표시되지 않는지 확인한다." + ], + "expected": "해당사례 주호소로 표면화조건과 혼동없음", + "side_effect": "none", + "required_data": "인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "C04", + "actor": "learner_test", + "goal": "다른사례 전환", + "preconditions": "인증된 학습자 테스트 계정", + "steps": [ + "첫 번째 사용 가능한 페르소나 option을 선택하고 미리보기 코드·이름·`주호소`를 기록한다.", + "다른 코드의 사용 가능한 option을 선택한다.", + "두 번째 카드만 `aria-selected=true`인지 확인한다.", + "미리보기 코드·이름·`주호소`와 `연습 기록`의 코드가 두 번째 선택과 일치하고 첫 번째 값이 남지 않는지 확인한다." + ], + "expected": "이전사례정보잔류없음", + "side_effect": "none", + "required_data": "인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "C05", + "actor": "learner_test", + "goal": "사례별 연습기록 보기", + "preconditions": "인증된 학습자 테스트 계정", + "steps": [ + "테스트 이력이 있는 페르소나 option을 선택한다.", + "`연습 기록` 영역의 `기존 회기` 코드와 회기 수를 확인한다.", + "각 `회기 기록 목록` 카드의 페르소나 코드가 선택한 카드 코드와 같은지 확인한다.", + "다른 페르소나를 선택한 뒤 해당 코드의 카드·회기 수로 바뀌는지 확인한다." + ], + "expected": "선택사례 기록만", + "side_effect": "none", + "required_data": "인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "C06", + "actor": "learner_test", + "goal": "첫연습 사례 안내", + "preconditions": "인증된 학습자 테스트 계정; 기록 0건인 활성 테스트 페르소나", + "steps": [ + "기록이 없는 테스트 페르소나 option을 선택한다.", + "`기존 회기` 수가 0인지와 빈 상태의 `첫 회기를 마치면 실제 저장 기록이 이곳에 쌓입니다.` 안내를 확인한다.", + "회기 방식의 `완전히 새로 시작`이 선택 가능한지 확인한다.", + "테스트 회기 생성 권한이 없으면 `완전히 새로 시작`을 누르지 않고 여기서 멈춘다." + ], + "expected": "기록없는사례 첫회기흐름", + "side_effect": "none", + "required_data": "기록 0건인 활성 테스트 페르소나", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "C07", + "actor": "learner_test", + "goal": "이력있는사례 이어가기 찾기", + "preconditions": "인증된 학습자 테스트 계정", + "steps": [ + "종료 또는 진행 중 이력이 있는 테스트 페르소나 option을 선택한다.", + "`회기 기록 목록`에서 진행 중 카드의 `이어하기`, 종료 카드의 `다음 회기 이어가기`, 리뷰 가능 카드의 `리뷰` 또는 `기록` 조작을 구분해 확인한다.", + "`회기 연속성` 영역이 표시되면 `이어서 진행` 라디오와 `이어서 진행할 사례` 선택 상자의 사용 가능 상태를 확인한다.", + "새 회기를 만들지 않고 기존 회기와 다음 회기 조작이 구분되는지만 기록한다." + ], + "expected": "기존회기/새회기 구분", + "side_effect": "none", + "required_data": "인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "C08", + "actor": "learner_test", + "goal": "사용불가사례 선택시도", + "preconditions": "인증된 학습자 테스트 계정; 사용 불가 상태의 사전등록 테스트 페르소나", + "steps": [ + "사용 불가 상태의 사전등록 테스트 페르소나가 목록에 있는지 확인한다.", + "해당 option의 `aria-disabled=true`, 비활성화 상태, `사용 불가` 배지를 확인한다.", + "비활성 카드에는 클릭으로 선택 상태가 생기지 않는지 확인한다.", + "미리보기의 `사용 가능한 내담자가 없습니다.` 또는 카탈로그 차단 안내와 시작 버튼 비활성 조건을 확인한다." + ], + "expected": "시작차단과 이유", + "side_effect": "none", + "required_data": "사용 불가 상태의 사전등록 테스트 페르소나", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "C09", + "actor": "learner_test", + "goal": "존재하지않는 사례 직접주소", + "preconditions": "인증된 학습자 테스트 계정", + "steps": [ + "인증된 학습자 테스트 계정으로 `/learn/session/존재하지않는사례코드`를 연다.", + "화면이 다른 실제 페르소나의 회기 준비·축어록으로 바뀌지 않는지 확인한다.", + "표시되는 오류·안전 복귀·로그인 화면 중 실제 결과의 URL과 문구를 기록한다." + ], + "expected": "다른사례로 몰래시작안함", + "side_effect": "none", + "required_data": "인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "C10", + "actor": "learner_test", + "goal": "추천사례에서 시작준비", + "preconditions": "인증된 학습자 테스트 계정", + "steps": [ + "인증된 학습자 테스트 계정으로 `/learn`을 연다.", + "`다음 연습 추천` 패널의 권장 문구와 `연습 시작하기` 또는 `리뷰 확인하기` 버튼을 확인한다.", + "`연습 시작하기`가 있을 때 누르고 `/learn/practice`로 이동한다.", + "이동한 미리보기 코드·이름이 추천에서 가리킨 대상과 같은지 확인하고 회기 생성 버튼은 누르지 않는다." + ], + "expected": "추천대상과 준비대상일치", + "side_effect": "none", + "required_data": "인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "C11", + "actor": "learner_test", + "goal": "사례선택 키보드조작", + "preconditions": "인증된 학습자 테스트 계정", + "steps": [ + "`/learn/practice`에서 Tab으로 `연습 페르소나`의 option 카드에 포커스를 이동한다.", + "Enter 또는 Space로 사용 가능한 option을 선택한다.", + "선택 카드의 `aria-selected=true`, 미리보기 변경, `완전히 새로 시작` 또는 `진행 중인 회기 이어가기` 버튼의 사용 가능 상태를 확인한다." + ], + "expected": "포커스/선택/준비가능", + "side_effect": "none", + "required_data": "인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "C12", + "actor": "learner_test", + "goal": "모바일 사례탐색", + "preconditions": "인증된 학습자 테스트 계정", + "steps": [ + "브라우저 폭을 390px variant로 설정하고 인증된 학습자 테스트 계정으로 `/learn/practice`를 연다.", + "`연습 페르소나`에서 사용 가능한 option을 선택한다.", + "미리보기의 `주호소`, `난도`, `접근`, 회기 방식, 시작 버튼까지 스크롤해 접근한다.", + "가로 스크롤 없이 모든 조작과 텍스트가 보이는지 확인하고 시작 버튼은 누르지 않는다." + ], + "expected": "선택/상세/준비 끝까지 사용가능", + "side_effect": "none", + "required_data": "인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "C13", + "actor": "learner_test", + "goal": "상세닫고 목록복귀", + "preconditions": "인증된 학습자 테스트 계정; 기능확인필요", + "steps": [ + "`/learn/practice`에서 사용 가능한 option을 선택해 오른쪽 미리보기를 연다.", + "소스에는 별도 상세 닫기 조작이 확인되지 않았으므로, 실제 배포 화면에 닫기 조작이 나타날 때만 그 정확한 라벨을 기록한다.", + "닫기 조작이 있으면 실행 뒤 `연습 페르소나` listbox와 기존 `aria-selected` 선택이 유지되는지 확인한다.", + "닫기 조작이 없으면 `기능확인필요`로 기록하고 존재하지 않는 버튼을 추정해 누르지 않는다." + ], + "expected": "선택맥락유지", + "side_effect": "none", + "required_data": "인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "C14", + "actor": "learner_test", + "goal": "시작전 이론모드 선택", + "preconditions": "인증된 학습자 테스트 계정; 소유자 허용 뒤 테스트 전용 계정·페르소나", + "steps": [ + "소유자가 허용한 테스트 전용 계정·페르소나를 선택한다.", + "회기 방식과 `완전히 새로 시작`을 확인한 뒤 테스트 회기 생성이 허용된 경우에만 시작한다.", + "회기 시작 전 준비 화면의 `이론모드 선택` fieldset에서 원하는 이론 버튼을 선택한다.", + "선택 상태가 준비 화면에 반영되는지 확인하고 실제 학습자 발화는 보내지 않는다." + ], + "expected": "준비화면에선택반영", + "side_effect": "test_data_mutation_requires_owner_approval", + "required_data": "소유자 허용 뒤 테스트 전용 계정·페르소나", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "C15", + "actor": "learner_test", + "goal": "새등록 테스트페르소나 선택", + "preconditions": "인증된 학습자 테스트 계정; 사전등록된 새 테스트 페르소나", + "steps": [ + "사전등록된 새 테스트 페르소나의 code를 가진 option을 `연습 페르소나`에서 선택한다.", + "미리보기 코드·이름·`주호소`, `난도`, `접근`, `음성`을 확인한다.", + "`완전히 새로 시작` 버튼이 사용 가능한지 확인한다.", + "회기 생성 허용이 없으면 버튼을 누르지 않고 내담자 정보와 준비 진입 조건만 기록한다." + ], + "expected": "내담자정보/상담준비 정상", + "side_effect": "none", + "required_data": "사전등록된 새 테스트 페르소나", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "C16", + "actor": "learner_test", + "goal": "퇴역테스트페르소나 deep link", + "preconditions": "인증된 학습자 테스트 계정; 퇴역 상태 테스트 페르소나 ID; 기능확인필요", + "steps": [ + "퇴역 상태의 사전등록 테스트 페르소나 code로 `/learn/session/{퇴역테스트페르소나ID}`를 연다.", + "다른 활성 페르소나의 회기 준비·축어록이 자동으로 열리지 않는지 확인한다.", + "화면에 나타난 차단·오류·안전 복귀 문구와 URL을 기록하고 신규 회기 시작 조작을 실행하지 않는다." + ], + "expected": "신규회기 차단", + "side_effect": "none", + "required_data": "퇴역 상태 테스트 페르소나 ID", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "C17", + "actor": "learner_test", + "goal": "두사례간 이력카운트대조", + "preconditions": "인증된 학습자 테스트 계정", + "steps": [ + "테스트 이력이 있는 첫 번째 페르소나 option을 선택한다.", + "`기존 회기` 수와 `회기 기록 목록` 카드의 code·턴 수를 기록한다.", + "두 번째 테스트 페르소나 option을 선택하고 같은 값을 기록한다.", + "각 목록 카드가 현재 선택 code만 가지며 두 대상의 수·카드가 섞이지 않는지 확인한다." + ], + "expected": "각기록 혼합없음", + "side_effect": "none", + "required_data": "인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "C18", + "actor": "learner_test", + "goal": "연습주소 새로고침", + "preconditions": "인증된 학습자 테스트 계정", + "steps": [ + "인증된 학습자 테스트 계정으로 `/learn/practice`를 연다.", + "사용 가능한 option을 선택해 `aria-selected=true`와 미리보기 code를 확인한다.", + "브라우저 새로고침을 한 번 실행한다.", + "목록 로딩 뒤 선택 option·미리보기·시작 버튼이 유효한 실제 카탈로그 상태인지 확인한다." + ], + "expected": "사례목록과 선택유효성 정상", + "side_effect": "none", + "required_data": "인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "S01", + "actor": "learner_test", + "goal": "시작전 안내확인", + "preconditions": "learner_test로 로그인한 실제 브라우저 세션; 실제 배포본 https://vignette.chanpaca.net; 시작하지 않은 테스트 전용 비식별 사례", + "steps": [ + "`/learn/practice`에서 테스트 전용 페르소나 option을 선택하고 `완전히 새로 시작`을 누른다.", + "`회기 시작 전 준비`의 `이번 회기 목표 선택`에서 목표 하나를 골라 첫 발화 기준을 확인한다.", + "`회기 시작`을 눌러 실제 회기 작업면을 연다.", + "`첫 발화 전에 내 기준을 잠급니다`에서 `목표`, `과업`, `유대` 각 fieldset의 radio를 하나씩 선택한다.", + "`기준 잠그고 첫 발화 준비`을 누른다.", + "`학습자 발화 입력`이 열리고 첫 발화 전 기준이 잠겼다는 상태를 확인한다." + ], + "expected": "대화전 준비상태명확", + "side_effect": "none", + "required_data": "시작 전 상태의 테스트 전용 비식별 사례", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "S02", + "actor": "learner_test", + "goal": "테스트회기1개 시작", + "preconditions": "learner_test로 로그인한 실제 브라우저 세션; 실제 배포본; 진행 중 회기가 없는 테스트 전용 비식별 사례", + "steps": [ + "`/learn/practice`에서 비식별 테스트 페르소나를 선택하고 `완전히 새로 시작`을 누른다.", + "`회기 시작 전 준비`의 `이번 회기 목표 선택`에서 목표 하나를 고른다.", + "`회기 시작`을 눌러 새 테스트 회기를 만든다.", + "`첫 발화 전에 내 기준을 잠급니다`에서 `목표`, `과업`, `유대`의 radio를 각각 하나씩 선택한다.", + "`기준 잠그고 첫 발화 준비`을 눌러 회기 전 판단을 잠근다.", + "표시된 페르소나 코드와 회기 URL의 식별값, `학습자 발화 입력`이 같은 새 회기임을 확인한다." + ], + "expected": "사례/회기 식별일치", + "side_effect": "test_data_mutation", + "required_data": "소유자 허용을 받은 미사용 테스트 계정과 비식별 사례", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "S03", + "actor": "learner_test", + "goal": "무입력전송", + "preconditions": "learner_test 로그인; 실제 배포본; 응답 생성 중이 아닌 활성 테스트 회기", + "steps": [ + "/learn/session/{테스트회기ID}에서 ‘학습자 발화 입력’의 현재 축어록 턴 수를 기록한다.", + "입력란을 비운 채 Enter를 누른다.", + "입력란과 ‘실시간 상담 축어록’을 다시 확인한다." + ], + "expected": "서버턴생성안함", + "side_effect": "none", + "required_data": "활성 테스트 회기와 전송 전 턴 수", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "S04", + "actor": "learner_test", + "goal": "공백만전송", + "preconditions": "learner_test 로그인; 실제 배포본; 응답 생성 중이 아닌 활성 테스트 회기", + "steps": [ + "‘학습자 발화 입력’에 공백만 입력하고 현재 턴 수를 기록한다.", + "Enter를 누른다.", + "공백이 새 학습자 발화나 내담자 응답으로 추가되지 않았는지 확인한다." + ], + "expected": "동일차단", + "side_effect": "none", + "required_data": "활성 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "S05", + "actor": "learner_test", + "goal": "한국어 첫발화전송", + "preconditions": "learner_test 로그인; 실제 배포본; 새 테스트 전용 비식별 회기", + "steps": [ + "‘학습자 발화 입력’에 ‘실습용 가상 사례에서 오늘의 기분을 한 문장으로 정리해 보고 싶어요.’를 입력한다.", + "Enter를 한 번 누른다.", + "축어록에 학습자 발화가 먼저 나타나고 실제 내담자 응답이 뒤따르는지 확인한다." + ], + "expected": "실제내담자응답 수신", + "side_effect": "test_data_mutation", + "required_data": "소유자 허용을 받은 새 테스트 회기와 비개인정보 한국어 입력", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "S06", + "actor": "learner_test", + "goal": "두번째맥락발화", + "preconditions": "learner_test 로그인; 실제 배포본; S05 뒤 내담자 응답이 완료된 동일 테스트 회기", + "steps": [ + "첫 왕복이 끝난 뒤 ‘학습자 발화 입력’에 ‘방금 말한 감정을 더 천천히 살펴보고 싶어요.’를 입력한다.", + "Enter를 한 번 누른다.", + "축어록의 새 두 턴이 기존 회기 아래에 이어지고 주소의 회기 ID가 바뀌지 않는지 확인한다." + ], + "expected": "같은회기 이어짐", + "side_effect": "test_data_mutation", + "required_data": "비개인정보 두 번째 한국어 입력과 기존 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "S07", + "actor": "learner_test", + "goal": "ShiftEnter 여러줄작성", + "preconditions": "learner_test 로그인; 실제 배포본; 활성 테스트 회기", + "steps": [ + "‘학습자 발화 입력’에 ‘실습용 첫 줄’을 입력한다.", + "Shift+Enter를 누르고 ‘실습용 둘째 줄’을 입력한다.", + "입력란에 두 줄이 유지되고 축어록에 새 턴이 생기지 않았는지 확인한 뒤 입력을 지운다." + ], + "expected": "전송전 줄바꿈보존", + "side_effect": "none", + "required_data": "활성 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "S08", + "actor": "learner_test", + "goal": "Enter전송", + "preconditions": "learner_test 로그인; 실제 배포본; 응답 생성 중이 아닌 활성 테스트 회기", + "steps": [ + "‘학습자 발화 입력’에 ‘실습용 가상 문장입니다. 다음 단계를 확인하고 싶어요.’를 입력한다.", + "Enter를 한 번 누른다.", + "입력란이 비고 축어록에 해당 학습자 발화가 정확히 한 번 나타나는지 확인한다.", + "내담자 응답 완료 뒤에도 같은 문장이 중복되지 않는지 확인한다." + ], + "expected": "1회전송", + "side_effect": "test_data_mutation", + "required_data": "비개인정보 한국어 입력과 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "S09", + "actor": "learner_test", + "goal": "빠른중복전송", + "preconditions": "learner_test 로그인; 실제 배포본; 응답 생성 중이 아닌 활성 테스트 회기", + "steps": [ + "‘학습자 발화 입력’에 ‘실습용 중복 방지 확인 문장입니다.’를 입력한다.", + "Enter를 빠르게 두 번 누른다.", + "축어록에서 해당 학습자 문장이 한 번만 추가되고 내담자 응답이 한 번만 시작되는지 확인한다." + ], + "expected": "중복학습자턴없음", + "side_effect": "test_data_mutation", + "required_data": "비개인정보 한국어 입력과 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "S10", + "actor": "learner_test", + "goal": "응답중 다음초안작성", + "preconditions": "learner_test 로그인; 실제 배포본; 자연 발생한 내담자 응답 생성 중인 테스트 회기", + "steps": [ + "내담자 응답이 축어록에 진행 중인 상태인지 확인한다.", + "‘학습자 발화 입력’에 ‘실습용 다음 초안이며 아직 보내지 않습니다.’를 입력한다.", + "내담자 응답이 끝난 뒤 입력란에 초안이 남아 있고 축어록에는 새 학습자 턴이 없는지 확인한다." + ], + "expected": "초안보존/실전송대기", + "side_effect": "none", + "required_data": "응답 생성 중 상태가 자연 발생한 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "S11", + "actor": "learner_test", + "goal": "긴정상길이발화", + "preconditions": "learner_test 로그인; 실제 배포본; 응답 생성 중이 아닌 테스트 회기", + "steps": [ + "사전 준비한 비개인정보 한국어 긴 문장(제품이 허용하는 길이)을 ‘학습자 발화 입력’에 붙여 넣는다.", + "Enter를 한 번 누른다.", + "학습자 발화와 내담자 응답의 줄바꿈·문장 끝이 보이고 가로 스크롤이 생기지 않는지 확인한다." + ], + "expected": "잘림/가로넘침없이수신", + "side_effect": "test_data_mutation", + "required_data": "제품 허용 길이 안의 비개인정보 긴 한국어 테스트 문자열", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "S12", + "actor": "learner_test", + "goal": "허용길이초과입력", + "preconditions": "learner_test 로그인; 실제 배포본; 운영에 전송하지 않아도 되는 테스트 회기; 제품 문자 한도 근거", + "steps": [ + "현재 Session 소스에는 ‘학습자 발화 입력’의 길이 제한이나 초과 차단 문구가 없음을 확인한다.", + "운영 LLM 전송 없이 검증할 수 있는 사전 설정 한도 데이터가 있는지 확인한다.", + "그 데이터가 없으면 긴 문자열을 제출하지 않고 BLOCKED로 기록한다." + ], + "expected": "UI차단/명확한제한(실LLM과다전송금지)", + "side_effect": "none", + "required_data": "프런트 또는 서버의 확인된 문자 한도와 비전송 검증 환경", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "S13", + "actor": "learner_test", + "goal": "한영숫자기호혼합", + "preconditions": "learner_test 로그인; 실제 배포본; 응답 생성 중이 아닌 테스트 회기", + "steps": [ + "‘학습자 발화 입력’에 ‘실습용 Mix A/B 42! #확인?’을 입력한다.", + "Enter를 한 번 누른다.", + "축어록의 학습자 턴에서 한글·영문·숫자·기호가 같은 순서로 보이는지 확인한다." + ], + "expected": "깨짐없이표시", + "side_effect": "test_data_mutation", + "required_data": "비개인정보 혼합 문자열과 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "S14", + "actor": "learner_test", + "goal": "위로스크롤하며대화읽기", + "preconditions": "learner_test 로그인; 실제 배포본; 여러 턴이 있는 테스트 회기", + "steps": [ + "‘실시간 상담 축어록’을 위로 스크롤해 과거 발화를 읽는다.", + "현재 스크롤 위치와 보이는 과거 발화를 확인한다.", + "자동으로 맨 아래로 이동하지 않는지 확인한다." + ], + "expected": "자동하단강제이동없음", + "side_effect": "none", + "required_data": "여러 턴이 저장된 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "S15", + "actor": "learner_test", + "goal": "최신으로복귀", + "preconditions": "learner_test 로그인; 실제 배포본; 위로 스크롤한 여러 턴 테스트 회기", + "steps": [ + "축어록을 위로 스크롤해 ‘최신으로’ 버튼을 표시한다.", + "‘최신으로’를 클릭한다.", + "마지막 축어록 턴이 보이고 ‘최신으로’ 버튼이 사라지는지 확인한다." + ], + "expected": "마지막턴으로돌아감", + "side_effect": "none", + "required_data": "여러 턴이 저장된 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "S16", + "actor": "learner_test", + "goal": "도움말 열고닫기", + "preconditions": "learner_test 로그인; 실제 배포본; 활성 테스트 회기", + "steps": [ + "‘학습자 발화 입력’에 전송하지 않을 ‘실습용 초안 보존 확인’ 문장을 입력한다.", + "? 또는 Ctrl+/를 눌러 ‘키보드 단축키 안내’ 대화상자를 연다.", + "Esc 또는 ‘확인 (Esc)’로 닫는다.", + "입력란의 초안과 기존 축어록이 그대로인지 확인한다." + ], + "expected": "작성문/대화유지", + "side_effect": "none", + "required_data": "활성 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "S17", + "actor": "learner_test", + "goal": "일시정지후재개", + "preconditions": "learner_test 로그인; 실제 배포본; 응답 생성 중이 아닌 활성 테스트 회기", + "steps": [ + "‘일시정지’를 클릭한다.", + "‘학습자 발화 입력’의 안내가 ‘일시정지 중입니다.’이고 입력이 막혔는지 확인한다.", + "‘이어가기’를 클릭한다.", + "입력란이 다시 활성화되고 상단 경과 표기가 일관되게 이어지는지 확인한다." + ], + "expected": "입력상태/시간흐름일관", + "side_effect": "none", + "required_data": "활성 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "S18", + "actor": "learner_test", + "goal": "회기설정열고취소", + "preconditions": "learner_test 로그인; 실제 배포본; 활성 테스트 회기", + "steps": [ + "현재 Session 소스에서 활성 회기 중 ‘회기 설정’ 열기·취소 조작이 제공되는지 확인한다.", + "확인된 활성 회기 설정 제어가 없으면 임의의 패널을 설정으로 간주하지 않는다.", + "발화·초안 보존을 검증하지 않고 BLOCKED로 기록한다." + ], + "expected": "발화/초안유지", + "side_effect": "none", + "required_data": "활성 회기 설정 제어의 실제 배포 증거", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "S19", + "actor": "learner_test", + "goal": "회기 새로고침", + "preconditions": "learner_test 로그인; 실제 배포본; 저장된 여러 턴이 있는 테스트 회기", + "steps": [ + "축어록의 학습자·내담자 턴 수와 마지막 문장을 기록한다.", + "브라우저 새로고침을 수행한다.", + "동일 /learn/session/{테스트회기ID}에서 기존 턴과 마지막 문장이 복원되고 중복 턴이 없는지 확인한다." + ], + "expected": "저장된턴복원/중복없음", + "side_effect": "none", + "required_data": "저장 완료된 여러 턴 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "S20", + "actor": "learner_test", + "goal": "다른화면후이어가기", + "preconditions": "learner_test 로그인; 실제 배포본; 저장된 턴이 있는 활성 테스트 회기", + "steps": [ + "상단 ‘기록으로’를 클릭해 /learn/history로 이동한다.", + "브라우저 뒤로 가기로 원래 /learn/session/{테스트회기ID}로 돌아온다.", + "상단의 ‘이전 회기 기록을 불러왔습니다.’ 또는 저장된 축어록을 확인하고 같은 회기 ID인지 대조한다." + ], + "expected": "같은회기복원", + "side_effect": "none", + "required_data": "저장된 턴이 있는 활성 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "S21", + "actor": "learner_test", + "goal": "정상종료확인", + "preconditions": "learner_test 로그인; 실제 배포본; 종료 가능한 테스트 전용 비식별 회기", + "steps": [ + "활성 회기에서 ‘종료하고 리뷰 보기’를 클릭한다.", + "‘회기를 종료할까요?’ 대화상자의 저장·리뷰 이동 안내를 확인한다.", + "대화상자의 ‘종료하고 리뷰 보기’를 한 번 클릭한다.", + "종료 상태와 /learn/session/{테스트회기ID}/review 이동을 확인한다." + ], + "expected": "종료저장/리뷰이동", + "side_effect": "test_data_mutation", + "required_data": "소유자 허용을 받은 종료 대상 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "S22", + "actor": "learner_test", + "goal": "종료취소", + "preconditions": "learner_test 로그인; 실제 배포본; 활성 테스트 회기", + "steps": [ + "‘종료하고 리뷰 보기’를 클릭해 ‘회기를 종료할까요?’ 대화상자를 연다.", + "‘계속 진행’을 클릭한다.", + "입력란이 ‘종료된 회기입니다.’ 상태가 아니고 기존 축어록이 유지되는지 확인한다." + ], + "expected": "진행상태유지", + "side_effect": "none", + "required_data": "활성 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "S23", + "actor": "learner_test", + "goal": "실제응답실패시복구", + "preconditions": "learner_test 로그인; 실제 배포본; 자연 발생한 내담자 응답 실패가 기록된 테스트 전용 회기", + "steps": [ + "자연 발생한 실패 배너와 ‘다시 전송’ 버튼이 보이는 회기인지 확인한다.", + "실패 전 입력한 비개인정보 발화가 복원됐는지 확인한다.", + "자연 실패 데이터가 없으면 장애를 유발하지 않고 BLOCKED로 기록한다." + ], + "expected": "초안복원/재전송/부분턴오염없음(실패미관찰이면BLOCKED,고의운영장애금지)", + "side_effect": "none", + "required_data": "자연 발생 응답 실패와 비개인정보 발화가 남은 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "S24", + "actor": "learner_test", + "goal": "발화없이종료한별도테스트회기", + "preconditions": "learner_test 로그인; 실제 배포본; 발화가 없는 별도 테스트 전용 회기", + "steps": [ + "축어록에 학습자·내담자 발화가 없는 새 테스트 회기인지 확인한다.", + "‘종료하고 리뷰 보기’를 열고 대화상자의 같은 버튼으로 종료를 확정한다.", + "리뷰에서 ‘축어록 없음’과 종료 회기 ID를 확인한다.", + "테스트 계정의 완료 수와 성취 배지가 늘지 않았는지 확인한다." + ], + "expected": "완료수와성취배지증가없음(추가생성필요)", + "side_effect": "test_data_mutation", + "required_data": "소유자 허용을 받은 발화 없는 별도 테스트 회기와 종료 전 계정 상태", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "R01", + "actor": "learner_test", + "goal": "종료리뷰열기", + "preconditions": "learner_test 로그인; 실제 배포본; 종료된 테스트 전용 회기", + "steps": [ + "/learn/session/{테스트회기ID}/review를 연다.", + "‘리뷰 생성 상태’와 화면 주소의 회기 ID를 확인한다.", + "표시된 사례 정보가 종료 전 회기와 같은지 확인한다." + ], + "expected": "해당사례/회기일치", + "side_effect": "none", + "required_data": "종료된 테스트 전용 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "R02", + "actor": "learner_test", + "goal": "축어록읽기", + "preconditions": "learner_test 로그인; 실제 배포본; 학습자·내담자 턴이 저장된 종료 테스트 회기", + "steps": [ + "리뷰의 ‘축어록’ 탭을 클릭한다.", + "‘전체’를 선택해 저장된 턴의 화자와 순서를 읽는다.", + "‘내 발화만’을 눌러 학습자 턴만 남는지 확인한 뒤 ‘전체’로 되돌린다." + ], + "expected": "실제보낸턴/응답순서보존", + "side_effect": "none", + "required_data": "순서를 알고 있는 저장 완료 테스트 축어록", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "R03", + "actor": "learner_test", + "goal": "리뷰새로고침", + "preconditions": "learner_test 로그인; 실제 배포본; 저장된 축어록과 리뷰 데이터가 있는 종료 테스트 회기", + "steps": [ + "‘축어록’ 탭에서 턴 수와 한 개의 비개인정보 문장을 기록한다.", + "브라우저를 새로고침한다.", + "동일 리뷰 주소에서 같은 탭을 열어 기록한 턴과 문장이 유지되는지 확인한다." + ], + "expected": "내용지속", + "side_effect": "none", + "required_data": "저장 완료된 종료 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "R04", + "actor": "learner_test", + "goal": "피드백탭", + "preconditions": "learner_test 로그인; 실제 배포본; 리뷰 생성 상태가 알려진 종료 테스트 회기", + "steps": [ + "‘피드백’ 탭을 클릭한다.", + "‘리뷰 생성 상태’의 완료·대기·실패 표시를 읽는다.", + "상태에 맞춰 ‘감정 타임라인 대기’, ‘평가 대기’, 또는 생성된 피드백 카드가 표시되는지 확인한다." + ], + "expected": "완료/대기/실패상태구분", + "side_effect": "none", + "required_data": "평가 상태가 확인된 종료 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "R05", + "actor": "learner_test", + "goal": "자기예측이유공란제출", + "preconditions": "learner_test 로그인; 실제 배포본; 예측 revision이 없는 종료 테스트 회기", + "steps": [ + "‘피드백’ 탭에서 ‘첫 자기예측 기록’을 찾는다.", + "‘성공 가능성’과 ‘이 예측에 대한 확신’을 조절하되 ‘그렇게 예상한 이유’는 비워 둔다.", + "‘첫 예측 기록’ 버튼이 비활성화되어 새 revision이 만들어지지 않는지 확인한다." + ], + "expected": "검증오류/기록생성없음", + "side_effect": "none", + "required_data": "예측 revision이 없는 종료 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "R06", + "actor": "learner_test", + "goal": "자기예측정상저장", + "preconditions": "learner_test 로그인; 실제 배포본; 예측 revision이 없는 종료 테스트 회기", + "steps": [ + "‘피드백’ 탭에서 ‘성공 가능성’과 ‘이 예측에 대한 확신’을 선택한다.", + "‘그렇게 예상한 이유’에 ‘비개인정보 실습 판단: 질문 뒤 감정을 요약하는 반응을 유지할 수 있습니다.’를 입력한다.", + "‘첫 예측 기록’을 한 번 클릭한다.", + "‘예측 revision을 기록했어. 외부평가 공개 전 잠금을 완료해줘.’ 상태 메시지를 확인한다." + ], + "expected": "점수/이유1회저장", + "side_effect": "test_data_mutation", + "required_data": "소유자 허용을 받은 미기록 종료 테스트 회기와 비개인정보 판단 이유", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "R07", + "actor": "learner_test", + "goal": "잠그기전수정", + "preconditions": "learner_test 로그인; 실제 배포본; 잠기지 않은 첫 예측이 있는 종료 테스트 회기", + "steps": [ + "‘피드백’ 탭에서 ‘성공 가능성’을 다른 5% 단위 값으로 바꾼다.", + "‘그렇게 예상한 이유’에 ‘비개인정보 실습 판단: 이전 판단을 더 보수적으로 조정합니다.’를 입력한다.", + "‘예측 수정 기록’을 한 번 클릭하고 새 revision 상태 메시지를 확인한다." + ], + "expected": "개정반영", + "side_effect": "test_data_mutation", + "required_data": "소유자 허용을 받은 잠기지 않은 예측과 비개인정보 판단 이유", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "R08", + "actor": "learner_test", + "goal": "자기예측명시잠금", + "preconditions": "learner_test 로그인; 실제 배포본; 잠기지 않은 최신 예측 revision이 있는 종료 테스트 회기", + "steps": [ + "‘명시적 잠금’의 ‘잠근 뒤 수정할 수 없음을 확인했어’ 체크박스를 선택한다.", + "활성화된 ‘이 예측 잠그기’를 한 번 클릭한다.", + "‘잠근 예측은 수정할 수 없어’와 ‘독립 수행 관찰’ 표시를 확인한다.", + "잠금 버튼이 다시 쓰기 동작을 만들지 않는지 확인한다." + ], + "expected": "잠금상태/중복잠금없음", + "side_effect": "test_data_mutation", + "required_data": "소유자 허용을 받은 잠기지 않은 예측 revision", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "R09", + "actor": "learner_test", + "goal": "잠금후새로고침", + "preconditions": "learner_test 로그인; 실제 배포본; R08에서 잠긴 예측이 있는 종료 테스트 회기", + "steps": [ + "‘피드백’ 탭의 잠금 표시를 확인한다.", + "브라우저를 새로고침한다.", + "다시 ‘피드백’ 탭을 열어 ‘예측 수정 기록’ 입력 폼이 없고 잠금 상태가 유지되는지 확인한다." + ], + "expected": "수정금지유지", + "side_effect": "none", + "required_data": "잠긴 예측이 저장된 종료 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "R10", + "actor": "learner_test", + "goal": "외부평가공개순서", + "preconditions": "learner_test 로그인; 실제 배포본; 같은 조건의 잠기지 않은 회기와 잠긴 회기 각 1개", + "steps": [ + "잠기지 않은 회기의 ‘피드백’ 탭에서 ‘아직 공개하지 않음’ 또는 ‘예측을 잠가야 비교 가능’을 확인한다.", + "잠긴 회기의 같은 카드를 열어 ‘독립 수행 관찰’과 허용된 평가 범위만 표시되는지 확인한다.", + "두 회기에서 실제 진단·인구집단 주장이 표시되지 않는지 확인한다." + ], + "expected": "잠금전비공개·잠금후허용범위", + "side_effect": "none", + "required_data": "잠금 전·후 상태가 분리된 종료 테스트 회기 2개", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "R11", + "actor": "learner_test", + "goal": "비식별가짜이름피드백", + "preconditions": "learner_test 로그인; 실제 배포본; [NAME]만 포함한 비식별 테스트 축어록이 있는 종료 회기", + "steps": [ + "‘축어록’ 탭을 연다.", + "[NAME]이 있던 테스트 문장이 ‘익명 인물은’ 또는 ‘익명 인물이’로 표시되는지 확인한다.", + "[NAME] 토큰이나 실제 이름이 화면에 남지 않는지 확인한다." + ], + "expected": "상담자/내담자역할치환·실명미노출(실제개인정보금지)", + "side_effect": "none", + "required_data": "실제 개인정보 없이 [NAME] 토큰만 가진 테스트 축어록", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "R12", + "actor": "learner_test", + "goal": "워크시트빈필수제출", + "preconditions": "learner_test 로그인; 실제 배포본; 워크시트 자동 초안이 있는 종료 테스트 회기", + "steps": [ + "‘워크시트’ 탭에서 ‘사례개념화 워크시트’를 연다.", + "현재 소스의 저장 버튼은 빈 필수값 검증이 아니라 변경 여부만 확인함을 확인한다.", + "빈 필수 제출용 검증 문구가 없으므로 임의 저장을 하지 않고 BLOCKED로 기록한다." + ], + "expected": "검증안내", + "side_effect": "none", + "required_data": "빈 필수값 검증이 실제로 배포된 테스트 환경", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "R13", + "actor": "learner_test", + "goal": "테스트워크시트작성저장", + "preconditions": "learner_test 로그인; 실제 배포본; 저장되지 않은 워크시트 자동 초안이 있는 종료 테스트 회기", + "steps": [ + "‘워크시트’ 탭에서 첫 번째 표시된 워크시트 textarea를 찾는다.", + "해당 aria-label이 나타내는 항목에 ‘비개인정보 실습 메모: 감정을 요약하고 개방형 질문을 이어간다.’를 입력한다.", + "활성화된 ‘저장’을 한 번 클릭한다.", + "상태가 ‘저장된 학습자 제출본’ 또는 ‘저장됨’으로 바뀌는지 확인한다." + ], + "expected": "저장성공", + "side_effect": "test_data_mutation", + "required_data": "소유자 허용을 받은 워크시트 자동 초안과 비개인정보 메모", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "R14", + "actor": "learner_test", + "goal": "워크시트다시열기", + "preconditions": "learner_test 로그인; 실제 배포본; R13 저장본이 있는 종료 테스트 회기", + "steps": [ + "‘워크시트’ 탭에서 저장한 비개인정보 메모를 확인한다.", + "‘축어록’ 탭으로 이동한 뒤 다시 ‘워크시트’ 탭을 연다.", + "상태가 ‘저장된 학습자 제출본’이고 저장한 문장이 유지되는지 확인한다." + ], + "expected": "내용보존", + "side_effect": "none", + "required_data": "저장 완료된 테스트 워크시트", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "R15", + "actor": "learner_test", + "goal": "피드백OFF 테스트계정", + "preconditions": "learner_test 로그인; 실제 배포본; learnerFeedbackEnabled=false로 사전 설정된 종료 테스트 계정·회기", + "steps": [ + "‘피드백’ 탭을 연다.", + "‘먼저 예측하고 잠그는 내 기록만 보존합니다’ 제목을 확인한다.", + "‘감정 밸런스 타임라인’, ‘좋았던 순간’, ‘다음 회기 개선점’ 등 파생 피드백 카드가 표시되지 않는지 확인한다." + ], + "expected": "피드백/파생카드차단(사전설정필요)", + "side_effect": "none", + "required_data": "피드백 OFF가 확인된 테스트 계정과 종료 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "R16", + "actor": "learner_test", + "goal": "피드백ON 테스트계정", + "preconditions": "learner_test 로그인; 실제 배포본; learnerFeedbackEnabled=true로 사전 설정된 종료 테스트 계정·회기", + "steps": [ + "‘피드백’ 탭을 연다.", + "‘먼저 예측하고, 잠근 뒤, 근거로 교정하기’ 제목을 확인한다.", + "평가 데이터가 있으면 해당 카드가, 없으면 ‘평가 대기’ 또는 ‘아직 생성된 내담자 피드백이 없습니다.’가 표시되는지 확인한다." + ], + "expected": "정상표시(사전설정필요)", + "side_effect": "none", + "required_data": "피드백 ON이 확인된 테스트 계정과 종료 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "R17", + "actor": "learner_test", + "goal": "리뷰에서다음회기준비", + "preconditions": "learner_test 로그인; 실제 배포본; 전이 또는 처방 반환 intent가 있는 종료 테스트 회기", + "steps": [ + "‘피드백’ 탭에서 반환 intent의 ‘같은 전이 과제로 다시 연습’ 또는 ‘같은 처방으로 다시 연습’을 찾는다.", + "링크의 출처·성공 기준 안내를 확인한다.", + "해당 링크를 열어 /learn/practice 주소에 원본 intent 검색값이 유지되는지 확인한다.", + "기존 리뷰 주소와 저장된 축어록을 다시 열어 남아 있는지 확인한다." + ], + "expected": "같은사례로이동·기존기록보존", + "side_effect": "none", + "required_data": "검증된 반환 intent가 있는 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "R18", + "actor": "learner_test", + "goal": "비교가능2회리뷰", + "preconditions": "learner_test 로그인; 실제 배포본; 같은 비식별 사례의 종료 테스트 회기 2개", + "steps": [ + "첫 번째 /review에서 ‘축어록’과 ‘피드백’의 회기 ID·근거 턴을 기록한다.", + "두 번째 /review를 열어 같은 항목을 기록한다.", + "각 화면이 자기 주소의 회기 ID와 축어록만 표시하고 근거가 섞이지 않는지 대조한다." + ], + "expected": "각회기근거구분", + "side_effect": "none", + "required_data": "같은 사례에 속한 종료 테스트 회기 2개", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "R19", + "actor": "learner_test", + "goal": "모바일리뷰탭/폼", + "preconditions": "learner_test 로그인; 실제 배포본; 390×844 모바일 뷰포트; 예측 입력 가능한 종료 테스트 회기", + "steps": [ + "390×844에서 /learn/session/{테스트회기ID}/review를 연다.", + "‘축어록’, ‘피드백’, ‘워크시트’ 탭을 차례로 눌러 각각의 패널이 보이는지 확인한다.", + "‘피드백’ 탭에서 ‘성공 가능성’과 ‘그렇게 예상한 이유’ 입력란을 스크롤해 완전히 조작 가능한지 확인한다.", + "가로 스크롤이나 화면 밖에 가려진 탭·입력란이 없는지 확인한다." + ], + "expected": "가로넘침/가려진조작없이완료", + "side_effect": "none", + "required_data": "모바일 뷰포트와 예측 입력 가능한 종료 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "R20", + "actor": "learner_test", + "goal": "평가실패회기재시도", + "preconditions": "learner_test 로그인; 실제 배포본; 자연 발생한 session_end 평가 실패가 기록된 테스트 전용 회기", + "steps": [ + "‘리뷰 생성 상태’에 실제 평가 실패와 재시도 제어가 보이는지 확인한다.", + "실패 원인 안내와 저장된 축어록을 읽는다.", + "자연 발생 실패 데이터가 없으면 재평가 요청이나 장애 유발을 하지 않고 BLOCKED로 기록한다." + ], + "expected": "상태안내·기록보존·다음회기막힘없음(실제실패데이터필요)", + "side_effect": "none", + "required_data": "자연 발생 session_end 평가 실패가 보존된 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "H01", + "actor": "learner_test", + "goal": "기록목록이동", + "preconditions": "인증된 학습자 테스트 계정", + "steps": [ + "인증된 학습자 테스트 계정으로 `/learn/history`를 연다.", + "`회기 기록을 찾고 정리합니다.` 제목과 `회기 기록` 영역을 확인한다.", + "`회기 기록 목록`의 각 카드에 현재 계정의 페르소나 code·상태·단계·턴 수·날짜가 표시되는지 확인한다." + ], + "expected": "실계정자기기록", + "side_effect": "none", + "required_data": "인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "H02", + "actor": "learner_test", + "goal": "기록검색", + "preconditions": "인증된 학습자 테스트 계정; 검색 가능한 테스트 페르소나 code 또는 상태 문자열", + "steps": [ + "`/learn/history`의 `aria-label=기록 검색` 입력에 테스트 페르소나 code 또는 상태 문자열을 입력한다.", + "목록 상단의 결과 수와 `회기 기록 목록` 카드를 확인한다.", + "표시된 카드의 code·단계·상태 중 적어도 하나가 입력한 검색어와 일치하는지 확인한다." + ], + "expected": "일치회기만", + "side_effect": "none", + "required_data": "검색 가능한 테스트 페르소나 code 또는 상태 문자열", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "H03", + "actor": "learner_test", + "goal": "검색결과없음", + "preconditions": "인증된 학습자 테스트 계정", + "steps": [ + "`기록 검색`에 존재하지 않는 임의 문자열을 입력한다.", + "빈 상태 문구와 결과 수 0을 확인한다.", + "입력 내용을 모두 지운다.", + "`전체 회기`과 기존 목록이 다시 표시되는지 확인한다." + ], + "expected": "빈상태와검색해제복구", + "side_effect": "none", + "required_data": "인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "H04", + "actor": "learner_test", + "goal": "진행중필터", + "preconditions": "인증된 학습자 테스트 계정; 진행 중 테스트 회기", + "steps": [ + "`/learn/history`의 `기록 상태 필터`에서 `진행 중` 버튼을 누른다.", + "버튼의 `aria-pressed=true`와 제목 `진행 중`을 확인한다.", + "각 결과 카드에 `이어하기`가 있고 종료 카드용 `리뷰`·`기록` 조작이 섞이지 않는지 확인한다." + ], + "expected": "이어가기대상만", + "side_effect": "none", + "required_data": "진행 중 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "H05", + "actor": "learner_test", + "goal": "종료필터", + "preconditions": "인증된 학습자 테스트 계정; 종료 테스트 회기; 기능확인필요; 기능확인필요", + "steps": [ + "`/learn/history`에서 종료된 테스트 기록을 먼저 찾는다.", + "소스의 상태 필터에는 `종료` 항목이 확인되지 않고 `전체`, `진행 중`, `리뷰 필요`, `보관됨`만 있으므로 실제 배포에서 `종료` 필터가 있는지 확인한다.", + "`종료` 필터가 있으면 정확한 버튼 라벨·결과 카드 상태를 기록한다.", + "없으면 기존 필터를 임의로 대체하지 않고 `기능확인필요`로 기록한다." + ], + "expected": "완료대상만", + "side_effect": "none", + "required_data": "종료 테스트 회기; 기능확인필요", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "H06", + "actor": "learner_test", + "goal": "리뷰필요필터", + "preconditions": "인증된 학습자 테스트 계정; 리뷰 준비 상태 테스트 회기", + "steps": [ + "`/learn/history`의 `기록 상태 필터`에서 `리뷰 필요` 버튼을 누른다.", + "`aria-pressed=true`와 목록 제목 `리뷰 필요`를 확인한다.", + "각 결과 카드가 `리뷰` 조작을 제공하고 `리뷰 가능` 상태인지 확인한다." + ], + "expected": "리뷰대상만", + "side_effect": "none", + "required_data": "리뷰 준비 상태 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "H07", + "actor": "learner_test", + "goal": "검색+상태조합", + "preconditions": "인증된 학습자 테스트 계정", + "steps": [ + "`기록 검색`에 테스트 페르소나 code를 입력한다.", + "`기록 상태 필터`에서 `진행 중` 또는 `리뷰 필요`를 선택한다.", + "검색어를 유지한 채 결과 카드가 선택한 상태와 code 조건을 모두 만족하는지 확인한다." + ], + "expected": "두조건유지", + "side_effect": "none", + "required_data": "인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "H08", + "actor": "learner_test", + "goal": "목록정렬", + "preconditions": "인증된 학습자 테스트 계정; 기능확인필요", + "steps": [ + "`/learn/history`를 열고 카드의 시작 날짜를 위에서 아래로 기록한다.", + "소스는 `sessionSortTime` 내림차순 정렬이며 UI 정렬 버튼은 확인되지 않았으므로 실제 배포에 정렬 조작이 있는지 확인한다.", + "정렬 조작이 있으면 라벨과 전후 날짜 순서를 기록한다.", + "정렬 조작이 없으면 날짜 내림차순 목록만 관찰하고 `기능확인필요`로 기록한다." + ], + "expected": "날짜기준일관", + "side_effect": "none", + "required_data": "인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "H09", + "actor": "learner_test", + "goal": "특정기록열고뒤로", + "preconditions": "인증된 학습자 테스트 계정", + "steps": [ + "검색어 또는 `리뷰 필요` 필터를 적용한 `/learn/history`에서 특정 카드의 `리뷰` 또는 `기록` 버튼을 누른다.", + "회기 리뷰의 URL에 해당 session ID가 포함되는지 확인한다.", + "브라우저 뒤로가기로 `/learn/history`에 돌아온다.", + "검색어·필터·결과 목록 맥락이 복귀 후 유지되는지 확인한다." + ], + "expected": "목록맥락복귀", + "side_effect": "none", + "required_data": "인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "H10", + "actor": "learner_test", + "goal": "테스트종료기록보관", + "preconditions": "인증된 학습자 테스트 계정; 소유자 허용 뒤 폐기 가능 종료 테스트 회기", + "steps": [ + "소유자가 명시적으로 허용한 폐기 가능 종료 테스트 회기만 `/learn/history`에서 찾는다.", + "대상 카드의 code·날짜·턴 수를 증거로 기록하고 `보관` 버튼을 확인한다.", + "허용이 없으면 버튼을 누르지 않고 safety gate와 `NOT_RUN`을 유지한다.", + "허용이 있으면 `보관`을 누르고 `회기를 보관했습니다.`와 카드의 `복원` 전환을 확인한다.", + "`보관됨` 필터에서 같은 code·날짜·턴 수의 원본 카드가 남아 있는지 확인한다." + ], + "expected": "보관목록이동·원본보존(사용자허용필요)", + "side_effect": "test_data_mutation_requires_owner_approval", + "required_data": "소유자 허용 뒤 폐기 가능 종료 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "H11", + "actor": "learner_test", + "goal": "보관기록복원", + "preconditions": "인증된 학습자 테스트 계정; 소유자 허용 뒤 보관된 폐기 가능 테스트 회기", + "steps": [ + "소유자가 명시적으로 허용한 보관된 폐기 가능 테스트 회기만 `보관됨` 필터에서 찾는다.", + "대상 카드의 code·날짜·턴 수를 기록하고 `복원` 버튼을 확인한다.", + "허용이 없으면 버튼을 누르지 않고 safety gate와 `NOT_RUN`을 유지한다.", + "허용이 있으면 `복원`을 누르고 `보관을 해제했습니다.`를 확인한다.", + "`전체 회기` 또는 원래 상태 필터에서 같은 원본 카드가 다시 보이는지 확인한다." + ], + "expected": "원래목록복귀", + "side_effect": "test_data_mutation_requires_owner_approval", + "required_data": "소유자 허용 뒤 보관된 폐기 가능 테스트 회기", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "H12", + "actor": "learner_test", + "goal": "설정화면조회", + "preconditions": "인증된 학습자 테스트 계정", + "steps": [ + "인증된 학습자 테스트 계정으로 `/settings`를 연다.", + "설정 레일의 `계정` 버튼을 선택한다.", + "`계정 요약`과 `표시 이름`, `이메일`, `소속` 필드를 확인한다.", + "이메일 입력이 비활성화된 로그인 계정 정보인지 확인한다." + ], + "expected": "자기계정정보", + "side_effect": "none", + "required_data": "인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "H13", + "actor": "learner_test", + "goal": "테스트계정프로필수정후복원", + "preconditions": "인증된 학습자 테스트 계정; 소유자 허용 뒤 원래 값을 복원할 수 있는 테스트 계정", + "steps": [ + "소유자가 허용한 테스트 계정으로 `/settings`의 `계정` 섹션을 연다.", + "`표시 이름`과 `소속`의 원래 값을 증거로 기록한다.", + "한 필드만 비식별 테스트 값으로 바꾸고 `저장`을 누른 뒤 `저장됨`을 확인한다.", + "새로고침 후 같은 테스트 값이 재조회되는지 확인한다.", + "원래 값을 다시 입력해 `저장`하고 새로고침하여 원복을 확인한다." + ], + "expected": "저장/재조회일관(기존값보존)", + "side_effect": "test_data_mutation_requires_owner_approval", + "required_data": "소유자 허용 뒤 원래 값을 복원할 수 있는 테스트 계정", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "H14", + "actor": "learner_test", + "goal": "설정변경취소", + "preconditions": "인증된 학습자 테스트 계정", + "steps": [ + "인증된 학습자 테스트 계정으로 `/settings`의 `계정` 섹션을 연다.", + "`표시 이름` 또는 `소속`에 저장하지 않을 임시 값을 입력한다.", + "사이드 레일의 다른 설정 섹션(예: `테마`)을 선택한 뒤 `계정`으로 돌아온다.", + "저장하지 않은 임시 값이 영속화되지 않았고 원래 값이 다시 표시되는지 확인한다." + ], + "expected": "기존값보존", + "side_effect": "none", + "required_data": "인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "T01", + "actor": "teacher_test", + "goal": "교수자로그인홈", + "preconditions": "승인된 teacher_test 계정으로 로그인 완료; 실제 배포본 https://vignette.chanpaca.net; 비식별 테스트 회기만 조회", + "steps": [ + "로그인된 teacher_test 세션에서 /teach 를 연다.", + "상단 역할 표기가 ‘교수자’이고 제목이 ‘교수 콘솔’인지 확인한다.", + "‘학생 분석’과 ‘새로고침’ 버튼이 표시되는지 확인한다.", + "주소가 /teach 이고 관리자 편집 제어가 보이지 않는지 기록한다." + ], + "expected": "허용콘솔", + "side_effect": "auth_session", + "required_data": "teacher_test 계정", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "T02", + "actor": "learner_test", + "goal": "학습자권한교수주소시도", + "preconditions": "인증된 learner_test 역할의 실제 브라우저 세션; admin 역할·admin 공간 전환으로 대체 검증 금지", + "steps": [ + "teacher_test 세션에서 /admin/users 주소를 직접 연다.", + "관리자 사용자 목록이나 ‘가입 승인과 권한 관리’가 표시되지 않는지 확인한다.", + "권한 오류, 차단 화면 또는 허용된 교수자 경로로의 전환을 확인한다.", + "주소와 화면의 접근 차단 상태를 기록한다." + ], + "expected": "차단", + "side_effect": "none", + "required_data": "learner_test 계정; 관리자 계정은 접근거부 PASS를 대체하지 않음", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "T03", + "actor": "teacher_test", + "goal": "대기목록에서테스트학습자리뷰", + "preconditions": "승인된 teacher_test 로그인 완료; 비식별 test_learner와 종료·review_ready 테스트 회기 1건이 교수자 큐에 존재", + "steps": [ + "/teach 에서 test_learner가 포함된 최근 회기 행을 찾는다.", + "그 행의 ‘상세 리뷰’ 동작을 선택한다.", + "주소가 /teach/session/{test_session_id}/review 이고 리뷰 헤더의 학습자·세션이 선택 행과 일치하는지 확인한다.", + "리뷰 생성 상태가 ‘준비됨’인지 확인한다." + ], + "expected": "대상일치", + "side_effect": "none", + "required_data": "test_learner 표시명, 종료·review_ready 테스트 session_id", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "T04", + "actor": "teacher_test", + "goal": "종료상태와리뷰상태구분", + "preconditions": "승인된 teacher_test 로그인 완료; 종료된 test_learner 회기 2건(검토 대기 1건, 검토 완료 또는 메모 저장 1건) 존재", + "steps": [ + "/teach 에서 두 테스트 회기의 종료 상태와 검토 배지를 확인한다.", + "검토 대기 행과 검토 완료 또는 메모 저장 행을 각각 연다.", + "각 상세 리뷰의 세션 종료 표시와 교수자 검토 상태가 행의 배지와 일치하는지 확인한다.", + "종료 여부와 검토 여부가 하나의 같은 상태로 합쳐져 표시되지 않는지 기록한다." + ], + "expected": "대기/완료명확", + "side_effect": "none", + "required_data": "상태가 다른 비식별 종료 테스트 회기 2건", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "T05", + "actor": "teacher_test", + "goal": "학습자검색", + "preconditions": "승인된 teacher_test 로그인 완료; test_learner의 표시명 또는 비식별 ID가 학생 분석 목록에 존재", + "steps": [ + "/teach/analysis 에서 ‘학습자 검색’ 입력칸을 찾는다.", + "test_learner의 고유 검색 문자열을 입력한다.", + "검색 가능한 학습자 추이 테이블에 해당 학습자만 남는지 확인한다.", + "남은 행의 표시명 또는 ID가 입력한 테스트 대상과 일치하는지 확인한다." + ], + "expected": "일치대상", + "side_effect": "none", + "required_data": "test_learner 표시명 또는 비식별 ID", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "T06", + "actor": "teacher_test", + "goal": "미일치검색복구", + "preconditions": "승인된 teacher_test 로그인 완료; 학생 분석 목록에 test_learner 존재", + "steps": [ + "/teach/analysis 의 ‘학습자 검색’에 존재하지 않는 비식별 문자열을 입력한다.", + "‘검색 결과 없음’ 빈 상태와 검색어를 줄이라는 안내를 확인한다.", + "입력값을 모두 지운다.", + "test_learner 행이 다시 표시되고 이전 빈 상태가 사라지는지 확인한다." + ], + "expected": "빈상태/해제정상", + "side_effect": "none", + "required_data": "존재하지 않는 비식별 검색 문자열, test_learner", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "T07", + "actor": "teacher_test", + "goal": "최신활동정렬", + "preconditions": "승인된 teacher_test 로그인 완료; 서로 다른 최근 활동 시각을 가진 비식별 학습자 2명 이상 존재", + "steps": [ + "/teach/analysis 의 ‘학습자 정렬’에서 최신 활동 정렬 제어를 선택한다.", + "목록 첫 두 행의 최근 활동 시각 또는 같은 기준으로 표시된 최신 회기를 확인한다.", + "첫 행이 둘째 행보다 오래되지 않았는지 확인한다.", + "정렬 전후 선택한 학습자 검색 조건이 유지되는지 확인한다." + ], + "expected": "목록정렬일관", + "side_effect": "none", + "required_data": "최근 활동 시각이 다른 test_learner 2명 이상", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "T08", + "actor": "teacher_test", + "goal": "회기수정렬", + "preconditions": "승인된 teacher_test 로그인 완료; 완료 회기 수가 다른 비식별 학습자 2명 이상 존재", + "steps": [ + "/teach/analysis 의 ‘학습자 정렬’에서 회기 수 정렬 제어를 선택한다.", + "표의 ‘회기’ 열에서 첫 두 행의 수치를 확인한다.", + "선택한 정렬 방향과 수치 순서가 일치하는지 확인한다.", + "행을 새로 열지 않아도 정렬이 화면 값만 바꾸고 데이터를 수정하지 않는지 확인한다." + ], + "expected": "값과순서일치", + "side_effect": "none", + "required_data": "완료 회기 수가 다른 test_learner 2명 이상", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "T09", + "actor": "teacher_test", + "goal": "학습자요약펼침닫힘", + "preconditions": "승인된 teacher_test 로그인 완료; 학생 분석 목록에 비식별 학습자 2명 이상 존재", + "steps": [ + "/teach/analysis 에서 첫 test_learner 행의 ‘요약 펼치기’ 버튼을 선택한다.", + "그 행에만 회기·점수·추이 요약이 열리는지 확인한다.", + "같은 행의 ‘요약 접기’를 선택한다.", + "둘째 행의 펼침 상태와 표시 값이 바뀌지 않았는지 확인한다." + ], + "expected": "다른행상태오염없음", + "side_effect": "none", + "required_data": "test_learner 2명 이상", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "T10", + "actor": "teacher_test", + "goal": "상세분석진입복귀", + "preconditions": "승인된 teacher_test 로그인 완료; test_learner가 학생 분석 목록에 존재", + "steps": [ + "/teach/analysis 에서 test_learner 행의 ‘상세 보기’를 선택한다.", + "상세 제목의 학습자 표시명이 선택한 행과 같은지 확인한다.", + "상단 ‘콘솔로’ 버튼을 선택한다.", + "/teach 로 돌아오고 로그인 역할이 교수자로 유지되는지 확인한다." + ], + "expected": "같은학습자맥락", + "side_effect": "none", + "required_data": "test_learner", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "T11", + "actor": "teacher_test", + "goal": "분석탭전환", + "preconditions": "승인된 teacher_test 로그인 완료; 상세 분석에 들어갈 test_learner 존재", + "steps": [ + "test_learner의 상세 분석을 연다.", + "‘학습자 상세 분석 탭’에서 ‘페르소나별 회기’, ‘회기별 추이’, ‘전체 회기’, ‘단계 분포’를 차례로 선택한다.", + "각 탭에서 선택된 탭만 활성 상태이고 해당 섹션 제목 또는 빈 상태가 보이는지 확인한다.", + "탭 전환 뒤 학습자 이름과 분석 요약 수치가 동일 대상의 값으로 유지되는지 확인한다." + ], + "expected": "선택에맞는데이터", + "side_effect": "none", + "required_data": "test_learner", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "T12", + "actor": "teacher_test", + "goal": "페르소나별기록그룹열기", + "preconditions": "승인된 teacher_test 로그인 완료; test_learner에 특정 페르소나 완료 회기 1건 이상 존재", + "steps": [ + "test_learner 상세의 ‘페르소나별 회기’ 탭을 연다.", + "대상 페르소나의 ‘회기 펼치기’ 버튼을 선택한다.", + "펼친 그룹의 회기 수와 대상 session_id 또는 회기 번호가 사전 설정 자료와 일치하는지 확인한다.", + "다른 페르소나 그룹을 열지 않은 상태에서 대상 그룹만 확장되는지 확인한다." + ], + "expected": "해당사례기록", + "side_effect": "none", + "required_data": "test_learner, 페르소나 코드, 해당 테스트 session_id", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "T13", + "actor": "teacher_test", + "goal": "회기타임라인개별리뷰", + "preconditions": "승인된 teacher_test 로그인 완료; test_learner의 종료·review_ready 테스트 session_id 존재", + "steps": [ + "test_learner 상세의 ‘전체 회기’ 탭을 연다.", + "대상 회기 번호의 ‘상세 리뷰’ 또는 ‘진행 기록’ 버튼을 선택한다.", + "주소의 session_id와 타임라인에서 선택한 test_session_id가 일치하는지 확인한다.", + "종료 회기라면 리뷰 화면의 축어록 또는 리뷰 준비 상태가 표시되는지 확인한다." + ], + "expected": "정확한회기", + "side_effect": "none", + "required_data": "test_learner, 종료·review_ready test_session_id", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "T14", + "actor": "teacher_test", + "goal": "추이차트부족데이터", + "preconditions": "승인된 teacher_test 로그인 완료; 평가 점수 또는 추이 표본이 부족한 test_learner 존재", + "steps": [ + "표본 부족 test_learner의 상세 분석에서 ‘회기별 추이’를 연다.", + "차트가 값 없는 항목을 ‘저장된 점수 없음’ 또는 중립 빈 상태로 표시하는지 확인한다.", + "‘상승’·‘하락’ 같은 근거 없는 추이 단정이 표시되지 않는지 확인한다.", + "다른 탭의 회기 수와 차트에 표시된 표본 수가 모순되지 않는지 확인한다." + ], + "expected": "무근거성과단정없음", + "side_effect": "none", + "required_data": "평가 표본 부족 test_learner", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "T15", + "actor": "teacher_test", + "goal": "테스트학습자검토메모임시작성취소", + "preconditions": "승인된 teacher_test 로그인 완료; 종료·review_ready test_session_id 존재", + "steps": [ + "대상 /teach/session/{test_session_id}/review 의 교수자 검토 영역을 연다.", + "‘교수자 검토 메모’에 ‘CUA 임시 메모 - 저장 금지’를 입력한다.", + "‘메모 저장’을 누르지 않고 ‘전체 회기’ 탭으로 이동한 뒤 같은 리뷰를 다시 연다.", + "임시 문자열과 저장 시각이 남지 않았는지 확인한다." + ], + "expected": "저장안됨", + "side_effect": "none", + "required_data": "비식별 종료 테스트 회기 1건", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "T16", + "actor": "teacher_test", + "goal": "테스트워크시트검토저장", + "preconditions": "승인된 teacher_test 로그인 완료; 소유자가 허용한 비식별 test_learner의 저장된 워크시트와 종료·review_ready test_session_id 존재; 테스트 뒤 정리 가능한 테스트 자료만 사용", + "steps": [ + "대상 /teach/session/{test_session_id}/review 에서 ‘워크시트 검수’를 연다.", + "‘워크시트 검수 메모’에 비식별 테스트 문자열을 입력한다.", + "‘수정요청’을 실제로 선택하고 저장 완료 상태 또는 검수 시각이 표시될 때까지 기다린다.", + "같은 URL을 새로고침하거나 목록에서 다시 열어 메모·수정요청 상태·검수 시각이 같은 test_session_id에만 남았는지 재조회한다.", + "다른 테스트 회기의 워크시트 상태가 바뀌지 않았는지 확인한다." + ], + "expected": "해당테스트항목만저장(승인필요)", + "side_effect": "test_data_mutation", + "required_data": "소유자 허용 비식별 워크시트 test_session_id, 저장된 학습자 워크시트", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "T17", + "actor": "teacher_test", + "goal": "안전큐대상열기", + "preconditions": "승인된 teacher_test 로그인 완료; 비식별 test_learner의 사전 설정 안전 알림 테스트 회기 존재", + "steps": [ + "/teach 의 ‘위기 알림’ 큐에서 비식별 테스트 항목만 찾는다.", + "항목의 학습자 라벨·페르소나 코드·위험도·session_id 앞부분이 사전 설정한 테스트 자료와 일치하는지 확인한다.", + "큐가 원문 축어록이나 실제 연락처를 노출하지 않고 안내 번호·리소스 제목만 표시하는지 확인한다.", + "알림 상태를 승인·해결로 변경하지 않고 화면을 닫는다." + ], + "expected": "해당테스트위기회기·안내(실제학생정보최소화)", + "side_effect": "none", + "required_data": "비식별 안전 알림 test_session_id", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "T18", + "actor": "teacher_test", + "goal": "감독·연구화면", + "preconditions": "승인된 teacher_test 로그인 완료; 실제 배포본 https://vignette.chanpaca.net", + "steps": [ + "교수자 사이드바에서 ‘감독·연구’를 선택하거나 /teach/supervision 을 연다.", + "감독 우선순위와 증거 매니페스트 영역이 표시되는지 확인한다.", + "관리자 전용 지속 개선 승인 원장이나 사용자 권한 편집 제어가 표시되지 않는지 확인한다.", + "교수자 역할 표기와 주소가 유지되는지 기록한다." + ], + "expected": "권한내기능표시", + "side_effect": "none", + "required_data": "teacher_test 계정", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "T19", + "actor": "teacher_test", + "goal": "모바일교수자목록검색리뷰", + "preconditions": "승인된 teacher_test 로그인 완료; 390px 폭; test_learner와 종료·review_ready test_session_id 존재", + "steps": [ + "390px 폭에서 /teach/analysis 를 열고 ‘학습자 검색’에 test_learner를 입력한다.", + "가로 스크롤 없이 대상 행의 ‘상세 보기’를 선택한다.", + "‘전체 회기’ 탭에서 대상 session_id의 상세 리뷰를 연다.", + "리뷰 헤더·탭·뒤로가기 동작이 화면 밖으로 잘리지 않는지 확인한다." + ], + "expected": "핵심동선조작가능", + "side_effect": "none", + "required_data": "test_learner, test_session_id", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "T20", + "actor": "teacher_test", + "goal": "교수자홈버튼", + "preconditions": "승인된 teacher_test 로그인 완료; 실제 배포본 https://vignette.chanpaca.net", + "steps": [ + "/teach/analysis 를 열어 ‘학생 분석’ 화면인지 확인한다.", + "헤더의 ‘콘솔로’ 버튼을 선택한다.", + "주소가 정확히 /teach 로 바뀌고 제목이 ‘교수 콘솔’로 바뀌는지 확인한다.", + "경로가 유지되거나 잘못된 화면으로 이동하면 URL·화면 제목을 불일치 증거로 기록한다." + ], + "expected": "정상/teach복귀(경로불일치후보확인)", + "side_effect": "none", + "required_data": "teacher_test 계정", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "O01", + "actor": "admin_test", + "goal": "관리자로그인홈", + "preconditions": "승인된 admin_test 계정으로 로그인 완료; 실제 배포본 https://vignette.chanpaca.net", + "steps": [ + "로그인된 admin_test 세션에서 /admin 을 연다.", + "역할 표기가 ‘관리자’이고 제목이 ‘현재 서비스 상태’인지 확인한다.", + "‘새로고침’과 운영 요약이 표시되는지 확인한다.", + "사용자 변경·운영 설정 저장 버튼은 누르지 않는다." + ], + "expected": "권한내콘솔", + "side_effect": "auth_session", + "required_data": "admin_test 계정", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "O02", + "actor": "learner_test", + "goal": "학습자관리주소", + "preconditions": "인증된 learner_test 역할의 실제 브라우저 세션; admin 역할·admin 공간 전환으로 대체 검증 금지", + "steps": [ + "admin_test 세션에서 /learn 주소를 직접 연다.", + "학습자 홈의 학습 시작 제어와 개인 회기 목록이 관리자에게 표시되지 않는지 확인한다.", + "권한 오류, 차단 화면 또는 허용된 관리자 경로 전환을 확인한다.", + "주소와 접근 차단 상태를 기록한다." + ], + "expected": "접근차단", + "side_effect": "none", + "required_data": "learner_test 계정; 관리자 계정은 접근거부 PASS를 대체하지 않음", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "O03", + "actor": "admin_test", + "goal": "운영홈서비스상태읽기", + "preconditions": "승인된 admin_test 로그인 완료; 실제 배포본 https://vignette.chanpaca.net", + "steps": [ + "/admin 의 ‘현재 서비스 상태’에서 상태 문구·환경·엔진 모드·갱신 시각을 읽는다.", + "‘새로고침’을 한 번 선택하고 완료 뒤 갱신 시각이 표시되는지 확인한다.", + "운영 요약의 활성 회기·온라인 사용자 수가 숫자 또는 명시적 빈 값으로 표시되는지 확인한다.", + "상태 값을 변경하거나 진단 조치를 실행하지 않는다." + ], + "expected": "실제상태/갱신시각명확", + "side_effect": "none", + "required_data": "admin_test 계정", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "O04", + "actor": "admin_test", + "goal": "AI운영기간변경", + "preconditions": "승인된 admin_test 로그인 완료; 실제 배포본 https://vignette.chanpaca.net", + "steps": [ + "/admin/ai 의 ‘계량 기간’에서 현재 선택값을 기록한다.", + "표시된 다른 기간 버튼을 선택한다.", + "‘일별 비용 흐름’ 차트의 aria-label과 기간 표시가 선택한 일수로 바뀌는지 확인한다.", + "‘최근 집계’ 시각과 모델별 사용 원장이 새 기간 기준으로 다시 표시되는지 확인한다." + ], + "expected": "집계기간반영", + "side_effect": "none", + "required_data": "admin_test 계정", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "O05", + "actor": "admin_test", + "goal": "모델별사용원장읽기", + "preconditions": "승인된 admin_test 로그인 완료; 선택 기간에 비식별 사용 원장 또는 명시적 빈 상태 존재", + "steps": [ + "/admin/ai 에서 ‘AI 사용 핵심 지표’와 모델별 사용 원장 영역을 연다.", + "모델명, 호출 수, 토큰 또는 비용 단위, 비용 기준 라벨이 함께 표시되는지 확인한다.", + "미계량 호출이 있으면 복원하지 않고 미계량이라는 설명이 표시되는지 확인한다.", + "원문 프롬프트·응답·개인 식별 정보가 표에 보이지 않는지 확인한다." + ], + "expected": "모델/계량단위명확", + "side_effect": "none", + "required_data": "admin_test 계정", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "O06", + "actor": "admin_test", + "goal": "AI설정열고취소", + "preconditions": "승인된 admin_test 로그인 완료; 현재 AI 엔진 설정이 조회 가능", + "steps": [ + "/admin/ai 의 ‘AI 운영 설정’에서 현재 공급자·연결 주소·기본 모델·추론 강도를 기록한다.", + "저장 제어를 누르지 않고 모델 목록 또는 설정 영역만 연다.", + "‘운영 설정 저장’을 선택하지 않은 채 /admin 으로 이동한다.", + "다시 /admin/ai 를 열어 기록한 설정 값이 변하지 않았는지 확인한다." + ], + "expected": "운영설정변경없음", + "side_effect": "none", + "required_data": "admin_test 계정, 현재 표시된 엔진 모드·모델 값", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "O07", + "actor": "admin_test", + "goal": "사용자검색", + "preconditions": "승인된 admin_test 로그인 완료; 비식별 test_learner 계정이 사용자 목록에 존재", + "steps": [ + "/admin/users 에서 ‘사용자 목록’ 탭을 선택한다.", + "‘사용자 검색’에 test_learner의 고유 비식별 검색 문자열을 입력한다.", + "남은 행의 표시 이름·역할이 사전 설정한 테스트 계정과 일치하는지 확인한다.", + "행의 저장·승인·역할 선택 제어는 누르지 않는다." + ], + "expected": "대상일치", + "side_effect": "none", + "required_data": "test_learner 표시명 또는 비식별 이메일 식별자", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "O08", + "actor": "admin_test", + "goal": "사용자검색없음", + "preconditions": "승인된 admin_test 로그인 완료; 사용자 목록 조회 가능", + "steps": [ + "/admin/users 의 ‘사용자 목록’ 탭에서 ‘사용자 검색’에 존재하지 않는 문자열을 입력한다.", + "검색 조건에 맞는 사용자가 없다는 빈 상태가 표시되는지 확인한다.", + "검색어를 지운다.", + "목록이 돌아오고 사용자 편집값이 바뀌지 않았는지 확인한다." + ], + "expected": "빈상태/복귀", + "side_effect": "none", + "required_data": "존재하지 않는 비식별 검색 문자열", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "O09", + "actor": "admin_test", + "goal": "사용자정렬", + "preconditions": "승인된 admin_test 로그인 완료; 사용자 목록에 비식별 계정 2개 이상 존재", + "steps": [ + "/admin/users 의 ‘사용자 목록’에서 정렬 가능한 열 머리글을 선택한다.", + "첫 두 행의 해당 열 값을 기록한다.", + "같은 열 머리글을 다시 선택해 정렬 방향을 바꾼다.", + "방향 표시와 두 행의 값 순서가 일치하고 사용자 편집 제어가 실행되지 않았는지 확인한다." + ], + "expected": "값/순서일관", + "side_effect": "none", + "required_data": "비식별 사용자 2개 이상", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "O10", + "actor": "admin_test", + "goal": "테스트사용자상세", + "preconditions": "승인된 admin_test 로그인 완료; 비식별 test_learner 계정이 사용자 목록에 존재", + "steps": [ + "/admin/users 의 ‘사용자 목록’에서 test_learner를 검색한다.", + "해당 행을 펼쳐 표시 이름, 역할, 승인 상태, ‘학습자 AI 피드백’ 값을 확인한다.", + "표시된 역할·피드백 상태가 사전 설정된 테스트 값과 일치하는지 확인한다.", + "select·switch·저장 버튼을 조작하지 않고 행을 다시 접는다." + ], + "expected": "역할/피드백상태확인", + "side_effect": "none", + "required_data": "test_learner 계정의 사전 설정 역할·AI 피드백 값", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "O11", + "actor": "admin_test", + "goal": "권한정책화면", + "preconditions": "승인된 admin_test 로그인 완료; 실제 배포본 https://vignette.chanpaca.net", + "steps": [ + "/admin/access 에서 ‘역할’ 탭을 연다.", + "관리자·교수자·학습자 정책 카드와 권한 수가 표시되는지 확인한다.", + "‘권한 매트릭스’ 탭을 선택해 리소스별 세 역할 열이 표시되는지 확인한다.", + "화면이 정책 표시 전용이며 역할·그룹 저장 API가 분리되지 않았다는 안내를 확인한다." + ], + "expected": "접근정책표시", + "side_effect": "none", + "required_data": "admin_test 계정", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "O12", + "actor": "admin_test", + "goal": "프로토콜메뉴접근", + "preconditions": "승인된 admin_test 로그인 완료; 프로토콜을 생성하지 않는 읽기 전용 확인", + "steps": [ + "/admin/access 에서 aria-label ‘접근 권한 탭’의 ‘상담 프로토콜’을 선택한다.", + "‘상담 프로토콜 초안 등록’ 제목과 ‘등록 프로토콜’ 목록이 함께 표시되는지 확인한다.", + "‘프로토콜 제목’, ‘프로토콜 출처’, ‘프로토콜 버전’, ‘프로토콜 라이선스’, ‘프로토콜 원문’ 입력 제어를 확인한다.", + "초안 등록·활성화·퇴역 버튼은 누르지 않는다." + ], + "expected": "목록/신규폼존재", + "side_effect": "none", + "required_data": "admin_test 계정", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "O13", + "actor": "admin_test", + "goal": "프로토콜검색상태필터", + "preconditions": "승인된 admin_test 로그인 완료; 제목 또는 상태가 알려진 비식별 테스트 프로토콜 존재", + "steps": [ + "/admin/access 의 ‘상담 프로토콜’ 탭에서 ‘프로토콜 검색’에 테스트 제목 조각을 입력한다.", + "‘프로토콜 상태 필터’를 사전 설정 상태로 선택한다.", + "남은 카드의 제목과 상태 배지가 둘 다 기대값과 일치하는지 확인한다.", + "검색어와 필터를 원래 값으로 되돌리고 목록이 복구되는지 확인한다." + ], + "expected": "일치항목", + "side_effect": "none", + "required_data": "비식별 테스트 프로토콜 제목 조각과 사전 설정 상태", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "O14", + "actor": "admin_test", + "goal": "프로토콜빈필수초안등록", + "preconditions": "승인된 admin_test 로그인 완료; 프로토콜 목록의 시작 개수 또는 대상 제목 부재를 기록", + "steps": [ + "/admin/access 의 ‘상담 프로토콜’ 탭에서 필수 입력을 모두 빈 값으로 둔다.", + "‘초안 등록’을 한 번 선택한다.", + "‘제목, 출처, 원문을 모두 입력해야 합니다.’ 오류가 표시되는지 확인한다.", + "등록 프로토콜 목록에 새 카드가 생기지 않았는지 확인한다." + ], + "expected": "오류안내·생성없음", + "side_effect": "none", + "required_data": "admin_test 계정", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "O15", + "actor": "admin_test", + "goal": "프로토콜초안작성취소", + "preconditions": "승인된 admin_test 로그인 완료; 프로토콜 목록과 기존 검색 결과를 기록", + "steps": [ + "‘상담 프로토콜 초안 등록’에 비식별 임시 제목·출처·원문을 입력한다.", + "‘초안 등록’을 누르지 않고 ‘역할’ 탭으로 이동한다.", + "다시 ‘상담 프로토콜’ 탭을 열어 임시 입력이 새 카드로 생성되지 않았는지 확인한다.", + "기존 프로토콜 카드 수와 검색 결과가 시작 기록과 같은지 확인한다." + ], + "expected": "기존자료영향없음", + "side_effect": "none", + "required_data": "비식별 임시 제목·출처·원문 문자열", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "O16", + "actor": "admin_test", + "goal": "티켓복합필터", + "preconditions": "승인된 admin_test 로그인 완료; 비식별 테스트 티켓이 사전 설정된 상태·카테고리·우선순위로 존재", + "steps": [ + "/admin/tickets 에서 ‘티켓 검색’에 테스트 제목 조각을 입력한다.", + "‘상태 필터’, ‘카테고리 필터’, ‘우선순위 필터’를 사전 설정값으로 선택한다.", + "미해결 운영 티켓 큐에 남은 카드가 목표 티켓과 일치하는지 확인한다.", + "티켓 상태 변경·해결 노트 저장 제어는 누르지 않는다." + ], + "expected": "일치티켓목록", + "side_effect": "none", + "required_data": "비식별 테스트 티켓의 제목 조각·상태·카테고리·우선순위", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "O17", + "actor": "admin_test", + "goal": "티켓상세열고복귀", + "preconditions": "승인된 admin_test 로그인 완료; O16의 복합 필터로 남는 비식별 테스트 티켓 존재", + "steps": [ + "/admin/tickets 에서 O16과 같은 검색·필터를 적용한다.", + "남은 테스트 티켓의 상세 열기 제어를 선택한다.", + "제목·상태·카테고리가 선택한 카드와 일치하는지 확인한다.", + "뒤로 또는 목록 복귀 후 검색어와 필터, 남은 카드가 유지되는지 확인한다." + ], + "expected": "필터맥락유지", + "side_effect": "none", + "required_data": "비식별 테스트 티켓", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "O18", + "actor": "admin_test", + "goal": "개선원장상태조회", + "preconditions": "승인된 admin_test 로그인 완료; 실제 배포본 https://vignette.chanpaca.net", + "steps": [ + "/admin/continuous-improvement 를 열고 ‘승격보다 근거를 먼저 본다’ 헤더를 확인한다.", + "운영 경계, 콘텐츠 검증 단계, 게이트 필수 증거, 수명주기 원장을 차례로 확인한다.", + "후보의 증거 상태와 사람 승인 이후 효과가 다른 영역으로 분리되어 있는지 확인한다.", + "‘원장 새로고침’ 외의 승인·보류·반려 제어는 누르지 않는다." + ], + "expected": "후보/증거/실행결정구분", + "side_effect": "none", + "required_data": "admin_test 계정", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "O19", + "actor": "admin_test", + "goal": "증거부족후보승인시도", + "preconditions": "승인된 admin_test 로그인 완료; 필수 증거가 부족한 비식별 후보가 지속 개선 원장에 존재", + "steps": [ + "/admin/continuous-improvement 에서 증거 부족 후보의 ‘게이트 필수 증거’ 상태를 확인한다.", + "‘필수 증거 4종 미완료로 승인 차단’ 또는 해당 차단 사유를 확인한다.", + "그 후보의 승인 버튼이 disabled 상태인지 접근성 상태와 화면으로 확인한다.", + "승인·보류·반려 선택이나 제출 버튼은 어떤 것도 누르지 않고 차단 관찰만 기록한다." + ], + "expected": "승인차단(실제승인제출금지)", + "side_effect": "none", + "required_data": "증거 부족 비식별 후보", + "status": "NOT_RUN", + "evidence": [] + }, + { + "id": "O20", + "actor": "admin_test", + "goal": "모바일관리자검색상세", + "preconditions": "승인된 admin_test 로그인 완료; 390px 폭; 비식별 test_learner 계정 존재", + "steps": [ + "390px 폭에서 /admin/users 의 ‘사용자 목록’ 탭을 연다.", + "‘사용자 검색’에 test_learner를 입력한다.", + "가로 스크롤 없이 대상 행의 상세를 열어 역할·승인 상태·학습자 AI 피드백 상태를 확인한다.", + "저장·승인·역할 변경 없이 상세를 닫고, 입력·필터·제어가 화면 밖으로 잘리지 않는지 확인한다." + ], + "expected": "가로넘침없이핵심조작", + "side_effect": "none", + "required_data": "test_learner 표시명 또는 비식별 이메일 식별자", + "status": "NOT_RUN", + "evidence": [] + } + ] +} diff --git a/docs/qa/production-computer-use-128-2026-09-09.md b/docs/qa/production-computer-use-128-2026-09-09.md new file mode 100644 index 0000000..c29ab5b --- /dev/null +++ b/docs/qa/production-computer-use-128-2026-09-09.md @@ -0,0 +1,1909 @@ +# 실배포 Computer Use 사용자 E2E 128 실행 카탈로그 + +- 대상: `https://vignette.chanpaca.net` 실제 배포본과 실제 백엔드 +- 관찰 asset: `/assets/index-p7x09Xs2.js`; commit SHA는 확인되지 않아 `null`이다. +- 방식: CUA 실제 브라우저. mock, `page.route`, localStorage 인증 조작, API 직접 write는 금지한다. +- 실행 원장: `outputs/production-cu-2026-09-09/ledger.json`이며 이 카탈로그는 실행 원장을 수정하지 않는다. +- baseline: 128개 모두 `NOT_RUN`, `evidence=[]`. 사전조건 불충족은 실행 시도에서만 `BLOCKED`이며 baseline을 바꾸지 않는다. +- 현재 허용: 로그인한 관리자 계정에서 비식별 검증 회기 2개, 가상 대화, 리뷰·워크시트 저장. 실제 사용자 데이터 수정, 운영 엔진 변경, 자료 공개, 승인 제출, 삭제는 실행하지 않는다. +- 역할 경계: learner_test·teacher_test 검증은 해당 역할 실제 세션으로 한다. 관리자 계정 또는 공간 전환은 접근거부 PASS의 대체가 아니다. +- source에서 확인되지 않은 기능은 `기능확인필요`로 두며 실제 UI가 확인되기 전 PASS로 처리하지 않는다. + +## 집계 +- 전체: 128 +- actor: admin_test 19, authenticated_role 3, learner_test 80, teacher_test 19, unauthenticated 7 +- side effect: auth_session 4, none 106, test_data_mutation 14, test_data_mutation_requires_owner_approval 4 + +## A · 인증 + +### A01 · 루트 비로그인 진입 + +- actor: `unauthenticated` +- 사전조건: 비로그인 새 브라우저 프로필 +- 단계: + 1. CUA의 새 비로그인 브라우저 프로필에서 `https://vignette.chanpaca.net/`를 연다. + 2. 주소가 `/login`으로 바뀔 때까지 기다린다. + 3. `로그인` 제목과 `Google 계정으로 로그인` 버튼을 확인한다. +- 기대결과: 로그인 화면/Google CTA +- side effect: `none` +- 필요 데이터: 비로그인 브라우저 상태; 계정 데이터 불필요 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### A02 · 학습홈 비로그인 직접주소 + +- actor: `unauthenticated` +- 사전조건: 비로그인 새 브라우저 프로필 +- 단계: + 1. 비로그인 새 프로필에서 `https://vignette.chanpaca.net/learn`를 연다. + 2. `/login`으로 이동한 뒤 로그인 패널의 `Google 계정으로 로그인` 버튼을 확인한다. + 3. 학습 대시보드·회기 기록·내담자 목록이 보이지 않는지 확인한다. +- 기대결과: 로그인으로 안전복귀 +- side effect: `none` +- 필요 데이터: 비로그인 브라우저 상태; 계정 데이터 불필요 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### A03 · 실제 존재 학습회기 deep link 비로그인 + +- actor: `unauthenticated` +- 사전조건: 비로그인 새 브라우저 프로필; 실제 존재하는 테스트 회기 ID +- 단계: + 1. 비로그인 새 프로필에서 실제 테스트 회기 ID를 넣은 `/learn/session/{테스트회기ID}`를 연다. + 2. `/login`으로 이동한 뒤 `Google 계정으로 로그인` 버튼을 확인한다. + 3. `실시간 상담 축어록`, `학습자 발화 입력`, 회기 내용이 로그인 전 화면에 없는지 확인한다. +- 기대결과: 내용 미노출 로그인 +- side effect: `none` +- 필요 데이터: 실제 존재하는 테스트 회기 ID(비식별); 비로그인 새 브라우저 프로필 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### A04 · 교수자콘솔 비로그인 + +- actor: `unauthenticated` +- 사전조건: 비로그인 새 브라우저 프로필 +- 단계: + 1. 비로그인 새 프로필에서 `https://vignette.chanpaca.net/teach`를 연다. + 2. `/login`으로 이동한 뒤 로그인 패널을 확인한다. + 3. `교수자 검토 요약`, `학습자 검색`, 학습자 행이 로그인 전 화면에 없는지 확인한다. +- 기대결과: 학생정보 미노출 로그인 +- side effect: `none` +- 필요 데이터: 비로그인 브라우저 상태; 계정 데이터 불필요 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### A05 · 관리자 비로그인 + +- actor: `unauthenticated` +- 사전조건: 비로그인 새 브라우저 프로필 +- 단계: + 1. 비로그인 새 프로필에서 `https://vignette.chanpaca.net/admin`를 연다. + 2. `/login`으로 이동한 뒤 로그인 패널을 확인한다. + 3. 관리자 사이드바의 `운영 홈`, `AI 운영`, `사용자`, `티켓`이 로그인 전 화면에 없는지 확인한다. +- 기대결과: 운영정보 미노출 로그인 +- side effect: `none` +- 필요 데이터: 비로그인 브라우저 상태; 계정 데이터 불필요 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### A06 · 로그인 키보드만 OAuth 진입 + +- actor: `unauthenticated` +- 사전조건: 비로그인 새 브라우저 프로필 +- 단계: + 1. 비로그인 새 프로필에서 `/login`을 연다. + 2. Tab으로 `Google 계정으로 로그인` 버튼에 가시 포커스를 이동한다. + 3. Enter를 눌러 `accounts.google.com`의 `/v3/signin/accountchooser` 인증 화면을 연다. + 4. 계정 선택·제출 전에 중지하고 host와 pathname, `계정을 선택하세요`만 증거로 남긴다. +- 기대결과: 포커스와 Google 인증화면 정상(계정선택 제출 전 중지 가능) +- side effect: `none` +- 필요 데이터: 비로그인 브라우저 상태; Google 계정 선택·제출 불필요 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### A07 · 일반Google 테스트계정 로그인 + +- actor: `learner_test` +- 사전조건: 해당 역할로 인증된 테스트 계정 +- 단계: + 1. `/login`에서 `Google 계정으로 로그인`을 누른다. + 2. 소유자가 일반 Google 테스트 계정의 자격증명을 직접 입력하고 인증을 완료한다. + 3. 역할 홈(`/learn`, `/teach`, `/admin` 중 해당 경로)에서 사용자 이름과 역할 공간을 확인한다. + 4. `승인된 이메일 도메인 또는 관리자 등록 계정만` 차단 문구가 표시되지 않는지 확인한다. +- 기대결과: 허용도메인차단없이 자기계정 진입 +- side effect: `auth_session` +- 필요 데이터: 소유자가 직접 입력하는 일반 Google 테스트 계정 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### A08 · OAuth 취소후 재시도 + +- actor: `unauthenticated` +- 사전조건: 비로그인 새 브라우저 프로필 +- 단계: + 1. 비로그인 상태에서 `/login`의 `Google 계정으로 로그인` 버튼을 누른다. + 2. Google 계정 선택 화면에서 제출하지 않고 브라우저 뒤로가기로 `/login`에 돌아온다. + 3. 같은 `Google 계정으로 로그인` 버튼을 다시 활성화한다. + 4. Google 인증 선택 화면이 다시 열리는지 확인한다. +- 기대결과: 로그인화면 복귀 후 재진입 가능 +- side effect: `none` +- 필요 데이터: 비로그인 브라우저 상태; Google 계정 선택·제출 불필요 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### A09 · 로그인된 사용자의 새탭 진입 + +- actor: `authenticated_role` +- 사전조건: 비로그인 또는 해당 역할 인증 상태의 브라우저; 실제 사용자 데이터는 읽지 않음 +- 단계: + 1. 인증된 테스트 계정이 있는 브라우저에서 새 탭을 연다. + 2. 새 탭 주소창에 `https://vignette.chanpaca.net/`를 입력한다. + 3. 현재 계정의 역할에 맞는 `/learn`, `/teach`, 또는 `/admin` 경로와 역할용 사이드바를 확인한다. +- 기대결과: 역할에 맞는 자기 화면 +- side effect: `none` +- 필요 데이터: 인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### A10 · 인증상태 새로고침 + +- actor: `authenticated_role` +- 사전조건: 해당 역할로 인증된 테스트 계정 +- 단계: + 1. 인증된 테스트 계정으로 역할 홈을 연다. + 2. 브라우저 새로고침을 한 번 실행한다. + 3. `/login`으로 반복 이동하지 않고 같은 역할 홈과 사용자 표시가 다시 나타나는지 확인한다. +- 기대결과: 로그아웃루프없이 복원 +- side effect: `none` +- 필요 데이터: 인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### A11 · 잘못된 앱주소 복귀 + +- actor: `authenticated_role` +- 사전조건: 기능확인필요 +- 단계: + 1. 인증 상태에 맞는 새 탭에서 `/e2e-not-found-20260909`를 연다. + 2. 라우터가 안전한 루트로 이동하는 동안 기다린다. + 3. 비로그인이면 `/login`과 `Google 계정으로 로그인`, 로그인 상태면 역할 홈을 확인해 앱 탐색을 재개한다. +- 기대결과: 404/안전한홈 이동으로 사용재개 +- side effect: `none` +- 필요 데이터: 잘못된 경로 문자열; 계정 데이터 불필요 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### A12 · 로그아웃후 뒤로가기 + +- actor: `learner_test` +- 사전조건: 해당 역할로 인증된 테스트 계정 +- 단계: + 1. 소유자가 허용한 폐기 가능 학습자 테스트 계정으로 `/learn`을 연다. + 2. 톱바의 `aria-label=로그아웃` 버튼을 누르고 `/login` 이동을 확인한다. + 3. 브라우저 뒤로가기를 한 번 실행한다. + 4. `실시간 상담 축어록`, `회기 기록`, `연습 페르소나` 같은 보호 내용이 다시 표시되지 않는지 확인한다. +- 기대결과: 보호된 내용 재노출없음(테스트계정만) +- side effect: `auth_session` +- 필요 데이터: 소유자가 허용한 폐기 가능 학습자 테스트 계정 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +## C · 사례 선택 + +### C01 · 학습홈에서 연습으로 이동 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정 +- 단계: + 1. 인증된 학습자 테스트 계정으로 `/learn`을 연다. + 2. 사이드바의 `학습` 링크를 선택한다. + 3. 주소가 `/learn/practice`인지와 `연습할 내담자를 선택합니다.` 제목을 확인한다. + 4. `연습 페르소나` listbox에 실제 서버에서 받은 option 카드가 표시되는지 확인한다. +- 기대결과: 실서버 사례목록 표시 +- side effect: `none` +- 필요 데이터: 인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### C02 · 제공 사례 선택 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정 +- 단계: + 1. `/learn/practice`의 `연습 페르소나` listbox에서 사용 가능한 option 카드 하나를 선택한다. + 2. 선택한 카드의 `aria-selected=true`와 체크 표시를 확인한다. + 3. 오른쪽 미리보기의 코드·이름이 선택 카드와 같은지 확인한다. + 4. `주호소` 아래 요약이 선택 카드의 `presenting_summary`와 일치하는지 확인한다. +- 기대결과: 선택명/미리보기 일치 +- side effect: `none` +- 필요 데이터: 인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### C03 · 사례 상세 주호소 확인 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정 +- 단계: + 1. `연습 페르소나`에서 사용 가능한 테스트 카드를 선택한다. + 2. 미리보기의 `주호소` 텍스트를 기록한다. + 3. 같은 미리보기에서 `난도`, `접근`, `음성` 값을 별도로 확인한다. + 4. 주호소 요약이 난도·접근 값과 섞여 한 필드로 표시되지 않는지 확인한다. +- 기대결과: 해당사례 주호소로 표면화조건과 혼동없음 +- side effect: `none` +- 필요 데이터: 인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### C04 · 다른사례 전환 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정 +- 단계: + 1. 첫 번째 사용 가능한 페르소나 option을 선택하고 미리보기 코드·이름·`주호소`를 기록한다. + 2. 다른 코드의 사용 가능한 option을 선택한다. + 3. 두 번째 카드만 `aria-selected=true`인지 확인한다. + 4. 미리보기 코드·이름·`주호소`와 `연습 기록`의 코드가 두 번째 선택과 일치하고 첫 번째 값이 남지 않는지 확인한다. +- 기대결과: 이전사례정보잔류없음 +- side effect: `none` +- 필요 데이터: 인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### C05 · 사례별 연습기록 보기 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정 +- 단계: + 1. 테스트 이력이 있는 페르소나 option을 선택한다. + 2. `연습 기록` 영역의 `기존 회기` 코드와 회기 수를 확인한다. + 3. 각 `회기 기록 목록` 카드의 페르소나 코드가 선택한 카드 코드와 같은지 확인한다. + 4. 다른 페르소나를 선택한 뒤 해당 코드의 카드·회기 수로 바뀌는지 확인한다. +- 기대결과: 선택사례 기록만 +- side effect: `none` +- 필요 데이터: 인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### C06 · 첫연습 사례 안내 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정; 기록 0건인 활성 테스트 페르소나 +- 단계: + 1. 기록이 없는 테스트 페르소나 option을 선택한다. + 2. `기존 회기` 수가 0인지와 빈 상태의 `첫 회기를 마치면 실제 저장 기록이 이곳에 쌓입니다.` 안내를 확인한다. + 3. 회기 방식의 `완전히 새로 시작`이 선택 가능한지 확인한다. + 4. 테스트 회기 생성 권한이 없으면 `완전히 새로 시작`을 누르지 않고 여기서 멈춘다. +- 기대결과: 기록없는사례 첫회기흐름 +- side effect: `none` +- 필요 데이터: 기록 0건인 활성 테스트 페르소나 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### C07 · 이력있는사례 이어가기 찾기 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정 +- 단계: + 1. 종료 또는 진행 중 이력이 있는 테스트 페르소나 option을 선택한다. + 2. `회기 기록 목록`에서 진행 중 카드의 `이어하기`, 종료 카드의 `다음 회기 이어가기`, 리뷰 가능 카드의 `리뷰` 또는 `기록` 조작을 구분해 확인한다. + 3. `회기 연속성` 영역이 표시되면 `이어서 진행` 라디오와 `이어서 진행할 사례` 선택 상자의 사용 가능 상태를 확인한다. + 4. 새 회기를 만들지 않고 기존 회기와 다음 회기 조작이 구분되는지만 기록한다. +- 기대결과: 기존회기/새회기 구분 +- side effect: `none` +- 필요 데이터: 인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### C08 · 사용불가사례 선택시도 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정; 사용 불가 상태의 사전등록 테스트 페르소나 +- 단계: + 1. 사용 불가 상태의 사전등록 테스트 페르소나가 목록에 있는지 확인한다. + 2. 해당 option의 `aria-disabled=true`, 비활성화 상태, `사용 불가` 배지를 확인한다. + 3. 비활성 카드에는 클릭으로 선택 상태가 생기지 않는지 확인한다. + 4. 미리보기의 `사용 가능한 내담자가 없습니다.` 또는 카탈로그 차단 안내와 시작 버튼 비활성 조건을 확인한다. +- 기대결과: 시작차단과 이유 +- side effect: `none` +- 필요 데이터: 사용 불가 상태의 사전등록 테스트 페르소나 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### C09 · 존재하지않는 사례 직접주소 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정 +- 단계: + 1. 인증된 학습자 테스트 계정으로 `/learn/session/존재하지않는사례코드`를 연다. + 2. 화면이 다른 실제 페르소나의 회기 준비·축어록으로 바뀌지 않는지 확인한다. + 3. 표시되는 오류·안전 복귀·로그인 화면 중 실제 결과의 URL과 문구를 기록한다. +- 기대결과: 다른사례로 몰래시작안함 +- side effect: `none` +- 필요 데이터: 인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### C10 · 추천사례에서 시작준비 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정 +- 단계: + 1. 인증된 학습자 테스트 계정으로 `/learn`을 연다. + 2. `다음 연습 추천` 패널의 권장 문구와 `연습 시작하기` 또는 `리뷰 확인하기` 버튼을 확인한다. + 3. `연습 시작하기`가 있을 때 누르고 `/learn/practice`로 이동한다. + 4. 이동한 미리보기 코드·이름이 추천에서 가리킨 대상과 같은지 확인하고 회기 생성 버튼은 누르지 않는다. +- 기대결과: 추천대상과 준비대상일치 +- side effect: `none` +- 필요 데이터: 인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### C11 · 사례선택 키보드조작 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정 +- 단계: + 1. `/learn/practice`에서 Tab으로 `연습 페르소나`의 option 카드에 포커스를 이동한다. + 2. Enter 또는 Space로 사용 가능한 option을 선택한다. + 3. 선택 카드의 `aria-selected=true`, 미리보기 변경, `완전히 새로 시작` 또는 `진행 중인 회기 이어가기` 버튼의 사용 가능 상태를 확인한다. +- 기대결과: 포커스/선택/준비가능 +- side effect: `none` +- 필요 데이터: 인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### C12 · 모바일 사례탐색 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정 +- 단계: + 1. 브라우저 폭을 390px variant로 설정하고 인증된 학습자 테스트 계정으로 `/learn/practice`를 연다. + 2. `연습 페르소나`에서 사용 가능한 option을 선택한다. + 3. 미리보기의 `주호소`, `난도`, `접근`, 회기 방식, 시작 버튼까지 스크롤해 접근한다. + 4. 가로 스크롤 없이 모든 조작과 텍스트가 보이는지 확인하고 시작 버튼은 누르지 않는다. +- 기대결과: 선택/상세/준비 끝까지 사용가능 +- side effect: `none` +- 필요 데이터: 인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### C13 · 상세닫고 목록복귀 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정; 기능확인필요 +- 단계: + 1. `/learn/practice`에서 사용 가능한 option을 선택해 오른쪽 미리보기를 연다. + 2. 소스에는 별도 상세 닫기 조작이 확인되지 않았으므로, 실제 배포 화면에 닫기 조작이 나타날 때만 그 정확한 라벨을 기록한다. + 3. 닫기 조작이 있으면 실행 뒤 `연습 페르소나` listbox와 기존 `aria-selected` 선택이 유지되는지 확인한다. + 4. 닫기 조작이 없으면 `기능확인필요`로 기록하고 존재하지 않는 버튼을 추정해 누르지 않는다. +- 기대결과: 선택맥락유지 +- side effect: `none` +- 필요 데이터: 인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### C14 · 시작전 이론모드 선택 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정; 소유자 허용 뒤 테스트 전용 계정·페르소나 +- 단계: + 1. 소유자가 허용한 테스트 전용 계정·페르소나를 선택한다. + 2. 회기 방식과 `완전히 새로 시작`을 확인한 뒤 테스트 회기 생성이 허용된 경우에만 시작한다. + 3. 회기 시작 전 준비 화면의 `이론모드 선택` fieldset에서 원하는 이론 버튼을 선택한다. + 4. 선택 상태가 준비 화면에 반영되는지 확인하고 실제 학습자 발화는 보내지 않는다. +- 기대결과: 준비화면에선택반영 +- side effect: `test_data_mutation_requires_owner_approval` +- 필요 데이터: 소유자 허용 뒤 테스트 전용 계정·페르소나 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### C15 · 새등록 테스트페르소나 선택 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정; 사전등록된 새 테스트 페르소나 +- 단계: + 1. 사전등록된 새 테스트 페르소나의 code를 가진 option을 `연습 페르소나`에서 선택한다. + 2. 미리보기 코드·이름·`주호소`, `난도`, `접근`, `음성`을 확인한다. + 3. `완전히 새로 시작` 버튼이 사용 가능한지 확인한다. + 4. 회기 생성 허용이 없으면 버튼을 누르지 않고 내담자 정보와 준비 진입 조건만 기록한다. +- 기대결과: 내담자정보/상담준비 정상 +- side effect: `none` +- 필요 데이터: 사전등록된 새 테스트 페르소나 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### C16 · 퇴역테스트페르소나 deep link + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정; 퇴역 상태 테스트 페르소나 ID; 기능확인필요 +- 단계: + 1. 퇴역 상태의 사전등록 테스트 페르소나 code로 `/learn/session/{퇴역테스트페르소나ID}`를 연다. + 2. 다른 활성 페르소나의 회기 준비·축어록이 자동으로 열리지 않는지 확인한다. + 3. 화면에 나타난 차단·오류·안전 복귀 문구와 URL을 기록하고 신규 회기 시작 조작을 실행하지 않는다. +- 기대결과: 신규회기 차단 +- side effect: `none` +- 필요 데이터: 퇴역 상태 테스트 페르소나 ID +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### C17 · 두사례간 이력카운트대조 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정 +- 단계: + 1. 테스트 이력이 있는 첫 번째 페르소나 option을 선택한다. + 2. `기존 회기` 수와 `회기 기록 목록` 카드의 code·턴 수를 기록한다. + 3. 두 번째 테스트 페르소나 option을 선택하고 같은 값을 기록한다. + 4. 각 목록 카드가 현재 선택 code만 가지며 두 대상의 수·카드가 섞이지 않는지 확인한다. +- 기대결과: 각기록 혼합없음 +- side effect: `none` +- 필요 데이터: 인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### C18 · 연습주소 새로고침 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정 +- 단계: + 1. 인증된 학습자 테스트 계정으로 `/learn/practice`를 연다. + 2. 사용 가능한 option을 선택해 `aria-selected=true`와 미리보기 code를 확인한다. + 3. 브라우저 새로고침을 한 번 실행한다. + 4. 목록 로딩 뒤 선택 option·미리보기·시작 버튼이 유효한 실제 카탈로그 상태인지 확인한다. +- 기대결과: 사례목록과 선택유효성 정상 +- side effect: `none` +- 필요 데이터: 인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +## S · 상담 + +### S01 · 시작전 안내확인 + +- actor: `learner_test` +- 사전조건: learner_test로 로그인한 실제 브라우저 세션; 실제 배포본 https://vignette.chanpaca.net; 시작하지 않은 테스트 전용 비식별 사례 +- 단계: + 1. `/learn/practice`에서 테스트 전용 페르소나 option을 선택하고 `완전히 새로 시작`을 누른다. + 2. `회기 시작 전 준비`의 `이번 회기 목표 선택`에서 목표 하나를 골라 첫 발화 기준을 확인한다. + 3. `회기 시작`을 눌러 실제 회기 작업면을 연다. + 4. `첫 발화 전에 내 기준을 잠급니다`에서 `목표`, `과업`, `유대` 각 fieldset의 radio를 하나씩 선택한다. + 5. `기준 잠그고 첫 발화 준비`을 누른다. + 6. `학습자 발화 입력`이 열리고 첫 발화 전 기준이 잠겼다는 상태를 확인한다. +- 기대결과: 대화전 준비상태명확 +- side effect: `none` +- 필요 데이터: 시작 전 상태의 테스트 전용 비식별 사례 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### S02 · 테스트회기1개 시작 + +- actor: `learner_test` +- 사전조건: learner_test로 로그인한 실제 브라우저 세션; 실제 배포본; 진행 중 회기가 없는 테스트 전용 비식별 사례 +- 단계: + 1. `/learn/practice`에서 비식별 테스트 페르소나를 선택하고 `완전히 새로 시작`을 누른다. + 2. `회기 시작 전 준비`의 `이번 회기 목표 선택`에서 목표 하나를 고른다. + 3. `회기 시작`을 눌러 새 테스트 회기를 만든다. + 4. `첫 발화 전에 내 기준을 잠급니다`에서 `목표`, `과업`, `유대`의 radio를 각각 하나씩 선택한다. + 5. `기준 잠그고 첫 발화 준비`을 눌러 회기 전 판단을 잠근다. + 6. 표시된 페르소나 코드와 회기 URL의 식별값, `학습자 발화 입력`이 같은 새 회기임을 확인한다. +- 기대결과: 사례/회기 식별일치 +- side effect: `test_data_mutation` +- 필요 데이터: 소유자 허용을 받은 미사용 테스트 계정과 비식별 사례 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### S03 · 무입력전송 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 응답 생성 중이 아닌 활성 테스트 회기 +- 단계: + 1. /learn/session/{테스트회기ID}에서 ‘학습자 발화 입력’의 현재 축어록 턴 수를 기록한다. + 2. 입력란을 비운 채 Enter를 누른다. + 3. 입력란과 ‘실시간 상담 축어록’을 다시 확인한다. +- 기대결과: 서버턴생성안함 +- side effect: `none` +- 필요 데이터: 활성 테스트 회기와 전송 전 턴 수 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### S04 · 공백만전송 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 응답 생성 중이 아닌 활성 테스트 회기 +- 단계: + 1. ‘학습자 발화 입력’에 공백만 입력하고 현재 턴 수를 기록한다. + 2. Enter를 누른다. + 3. 공백이 새 학습자 발화나 내담자 응답으로 추가되지 않았는지 확인한다. +- 기대결과: 동일차단 +- side effect: `none` +- 필요 데이터: 활성 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### S05 · 한국어 첫발화전송 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 새 테스트 전용 비식별 회기 +- 단계: + 1. ‘학습자 발화 입력’에 ‘실습용 가상 사례에서 오늘의 기분을 한 문장으로 정리해 보고 싶어요.’를 입력한다. + 2. Enter를 한 번 누른다. + 3. 축어록에 학습자 발화가 먼저 나타나고 실제 내담자 응답이 뒤따르는지 확인한다. +- 기대결과: 실제내담자응답 수신 +- side effect: `test_data_mutation` +- 필요 데이터: 소유자 허용을 받은 새 테스트 회기와 비개인정보 한국어 입력 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### S06 · 두번째맥락발화 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; S05 뒤 내담자 응답이 완료된 동일 테스트 회기 +- 단계: + 1. 첫 왕복이 끝난 뒤 ‘학습자 발화 입력’에 ‘방금 말한 감정을 더 천천히 살펴보고 싶어요.’를 입력한다. + 2. Enter를 한 번 누른다. + 3. 축어록의 새 두 턴이 기존 회기 아래에 이어지고 주소의 회기 ID가 바뀌지 않는지 확인한다. +- 기대결과: 같은회기 이어짐 +- side effect: `test_data_mutation` +- 필요 데이터: 비개인정보 두 번째 한국어 입력과 기존 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### S07 · ShiftEnter 여러줄작성 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 활성 테스트 회기 +- 단계: + 1. ‘학습자 발화 입력’에 ‘실습용 첫 줄’을 입력한다. + 2. Shift+Enter를 누르고 ‘실습용 둘째 줄’을 입력한다. + 3. 입력란에 두 줄이 유지되고 축어록에 새 턴이 생기지 않았는지 확인한 뒤 입력을 지운다. +- 기대결과: 전송전 줄바꿈보존 +- side effect: `none` +- 필요 데이터: 활성 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### S08 · Enter전송 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 응답 생성 중이 아닌 활성 테스트 회기 +- 단계: + 1. ‘학습자 발화 입력’에 ‘실습용 가상 문장입니다. 다음 단계를 확인하고 싶어요.’를 입력한다. + 2. Enter를 한 번 누른다. + 3. 입력란이 비고 축어록에 해당 학습자 발화가 정확히 한 번 나타나는지 확인한다. + 4. 내담자 응답 완료 뒤에도 같은 문장이 중복되지 않는지 확인한다. +- 기대결과: 1회전송 +- side effect: `test_data_mutation` +- 필요 데이터: 비개인정보 한국어 입력과 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### S09 · 빠른중복전송 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 응답 생성 중이 아닌 활성 테스트 회기 +- 단계: + 1. ‘학습자 발화 입력’에 ‘실습용 중복 방지 확인 문장입니다.’를 입력한다. + 2. Enter를 빠르게 두 번 누른다. + 3. 축어록에서 해당 학습자 문장이 한 번만 추가되고 내담자 응답이 한 번만 시작되는지 확인한다. +- 기대결과: 중복학습자턴없음 +- side effect: `test_data_mutation` +- 필요 데이터: 비개인정보 한국어 입력과 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### S10 · 응답중 다음초안작성 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 자연 발생한 내담자 응답 생성 중인 테스트 회기 +- 단계: + 1. 내담자 응답이 축어록에 진행 중인 상태인지 확인한다. + 2. ‘학습자 발화 입력’에 ‘실습용 다음 초안이며 아직 보내지 않습니다.’를 입력한다. + 3. 내담자 응답이 끝난 뒤 입력란에 초안이 남아 있고 축어록에는 새 학습자 턴이 없는지 확인한다. +- 기대결과: 초안보존/실전송대기 +- side effect: `none` +- 필요 데이터: 응답 생성 중 상태가 자연 발생한 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### S11 · 긴정상길이발화 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 응답 생성 중이 아닌 테스트 회기 +- 단계: + 1. 사전 준비한 비개인정보 한국어 긴 문장(제품이 허용하는 길이)을 ‘학습자 발화 입력’에 붙여 넣는다. + 2. Enter를 한 번 누른다. + 3. 학습자 발화와 내담자 응답의 줄바꿈·문장 끝이 보이고 가로 스크롤이 생기지 않는지 확인한다. +- 기대결과: 잘림/가로넘침없이수신 +- side effect: `test_data_mutation` +- 필요 데이터: 제품 허용 길이 안의 비개인정보 긴 한국어 테스트 문자열 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### S12 · 허용길이초과입력 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 운영에 전송하지 않아도 되는 테스트 회기; 제품 문자 한도 근거 +- 단계: + 1. 현재 Session 소스에는 ‘학습자 발화 입력’의 길이 제한이나 초과 차단 문구가 없음을 확인한다. + 2. 운영 LLM 전송 없이 검증할 수 있는 사전 설정 한도 데이터가 있는지 확인한다. + 3. 그 데이터가 없으면 긴 문자열을 제출하지 않고 BLOCKED로 기록한다. +- 기대결과: UI차단/명확한제한(실LLM과다전송금지) +- side effect: `none` +- 필요 데이터: 프런트 또는 서버의 확인된 문자 한도와 비전송 검증 환경 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### S13 · 한영숫자기호혼합 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 응답 생성 중이 아닌 테스트 회기 +- 단계: + 1. ‘학습자 발화 입력’에 ‘실습용 Mix A/B 42! #확인?’을 입력한다. + 2. Enter를 한 번 누른다. + 3. 축어록의 학습자 턴에서 한글·영문·숫자·기호가 같은 순서로 보이는지 확인한다. +- 기대결과: 깨짐없이표시 +- side effect: `test_data_mutation` +- 필요 데이터: 비개인정보 혼합 문자열과 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### S14 · 위로스크롤하며대화읽기 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 여러 턴이 있는 테스트 회기 +- 단계: + 1. ‘실시간 상담 축어록’을 위로 스크롤해 과거 발화를 읽는다. + 2. 현재 스크롤 위치와 보이는 과거 발화를 확인한다. + 3. 자동으로 맨 아래로 이동하지 않는지 확인한다. +- 기대결과: 자동하단강제이동없음 +- side effect: `none` +- 필요 데이터: 여러 턴이 저장된 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### S15 · 최신으로복귀 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 위로 스크롤한 여러 턴 테스트 회기 +- 단계: + 1. 축어록을 위로 스크롤해 ‘최신으로’ 버튼을 표시한다. + 2. ‘최신으로’를 클릭한다. + 3. 마지막 축어록 턴이 보이고 ‘최신으로’ 버튼이 사라지는지 확인한다. +- 기대결과: 마지막턴으로돌아감 +- side effect: `none` +- 필요 데이터: 여러 턴이 저장된 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### S16 · 도움말 열고닫기 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 활성 테스트 회기 +- 단계: + 1. ‘학습자 발화 입력’에 전송하지 않을 ‘실습용 초안 보존 확인’ 문장을 입력한다. + 2. ? 또는 Ctrl+/를 눌러 ‘키보드 단축키 안내’ 대화상자를 연다. + 3. Esc 또는 ‘확인 (Esc)’로 닫는다. + 4. 입력란의 초안과 기존 축어록이 그대로인지 확인한다. +- 기대결과: 작성문/대화유지 +- side effect: `none` +- 필요 데이터: 활성 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### S17 · 일시정지후재개 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 응답 생성 중이 아닌 활성 테스트 회기 +- 단계: + 1. ‘일시정지’를 클릭한다. + 2. ‘학습자 발화 입력’의 안내가 ‘일시정지 중입니다.’이고 입력이 막혔는지 확인한다. + 3. ‘이어가기’를 클릭한다. + 4. 입력란이 다시 활성화되고 상단 경과 표기가 일관되게 이어지는지 확인한다. +- 기대결과: 입력상태/시간흐름일관 +- side effect: `none` +- 필요 데이터: 활성 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### S18 · 회기설정열고취소 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 활성 테스트 회기 +- 단계: + 1. 현재 Session 소스에서 활성 회기 중 ‘회기 설정’ 열기·취소 조작이 제공되는지 확인한다. + 2. 확인된 활성 회기 설정 제어가 없으면 임의의 패널을 설정으로 간주하지 않는다. + 3. 발화·초안 보존을 검증하지 않고 BLOCKED로 기록한다. +- 기대결과: 발화/초안유지 +- side effect: `none` +- 필요 데이터: 활성 회기 설정 제어의 실제 배포 증거 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### S19 · 회기 새로고침 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 저장된 여러 턴이 있는 테스트 회기 +- 단계: + 1. 축어록의 학습자·내담자 턴 수와 마지막 문장을 기록한다. + 2. 브라우저 새로고침을 수행한다. + 3. 동일 /learn/session/{테스트회기ID}에서 기존 턴과 마지막 문장이 복원되고 중복 턴이 없는지 확인한다. +- 기대결과: 저장된턴복원/중복없음 +- side effect: `none` +- 필요 데이터: 저장 완료된 여러 턴 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### S20 · 다른화면후이어가기 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 저장된 턴이 있는 활성 테스트 회기 +- 단계: + 1. 상단 ‘기록으로’를 클릭해 /learn/history로 이동한다. + 2. 브라우저 뒤로 가기로 원래 /learn/session/{테스트회기ID}로 돌아온다. + 3. 상단의 ‘이전 회기 기록을 불러왔습니다.’ 또는 저장된 축어록을 확인하고 같은 회기 ID인지 대조한다. +- 기대결과: 같은회기복원 +- side effect: `none` +- 필요 데이터: 저장된 턴이 있는 활성 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### S21 · 정상종료확인 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 종료 가능한 테스트 전용 비식별 회기 +- 단계: + 1. 활성 회기에서 ‘종료하고 리뷰 보기’를 클릭한다. + 2. ‘회기를 종료할까요?’ 대화상자의 저장·리뷰 이동 안내를 확인한다. + 3. 대화상자의 ‘종료하고 리뷰 보기’를 한 번 클릭한다. + 4. 종료 상태와 /learn/session/{테스트회기ID}/review 이동을 확인한다. +- 기대결과: 종료저장/리뷰이동 +- side effect: `test_data_mutation` +- 필요 데이터: 소유자 허용을 받은 종료 대상 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### S22 · 종료취소 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 활성 테스트 회기 +- 단계: + 1. ‘종료하고 리뷰 보기’를 클릭해 ‘회기를 종료할까요?’ 대화상자를 연다. + 2. ‘계속 진행’을 클릭한다. + 3. 입력란이 ‘종료된 회기입니다.’ 상태가 아니고 기존 축어록이 유지되는지 확인한다. +- 기대결과: 진행상태유지 +- side effect: `none` +- 필요 데이터: 활성 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### S23 · 실제응답실패시복구 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 자연 발생한 내담자 응답 실패가 기록된 테스트 전용 회기 +- 단계: + 1. 자연 발생한 실패 배너와 ‘다시 전송’ 버튼이 보이는 회기인지 확인한다. + 2. 실패 전 입력한 비개인정보 발화가 복원됐는지 확인한다. + 3. 자연 실패 데이터가 없으면 장애를 유발하지 않고 BLOCKED로 기록한다. +- 기대결과: 초안복원/재전송/부분턴오염없음(실패미관찰이면BLOCKED,고의운영장애금지) +- side effect: `none` +- 필요 데이터: 자연 발생 응답 실패와 비개인정보 발화가 남은 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### S24 · 발화없이종료한별도테스트회기 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 발화가 없는 별도 테스트 전용 회기 +- 단계: + 1. 축어록에 학습자·내담자 발화가 없는 새 테스트 회기인지 확인한다. + 2. ‘종료하고 리뷰 보기’를 열고 대화상자의 같은 버튼으로 종료를 확정한다. + 3. 리뷰에서 ‘축어록 없음’과 종료 회기 ID를 확인한다. + 4. 테스트 계정의 완료 수와 성취 배지가 늘지 않았는지 확인한다. +- 기대결과: 완료수와성취배지증가없음(추가생성필요) +- side effect: `test_data_mutation` +- 필요 데이터: 소유자 허용을 받은 발화 없는 별도 테스트 회기와 종료 전 계정 상태 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +## R · 리뷰 + +### R01 · 종료리뷰열기 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 종료된 테스트 전용 회기 +- 단계: + 1. /learn/session/{테스트회기ID}/review를 연다. + 2. ‘리뷰 생성 상태’와 화면 주소의 회기 ID를 확인한다. + 3. 표시된 사례 정보가 종료 전 회기와 같은지 확인한다. +- 기대결과: 해당사례/회기일치 +- side effect: `none` +- 필요 데이터: 종료된 테스트 전용 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### R02 · 축어록읽기 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 학습자·내담자 턴이 저장된 종료 테스트 회기 +- 단계: + 1. 리뷰의 ‘축어록’ 탭을 클릭한다. + 2. ‘전체’를 선택해 저장된 턴의 화자와 순서를 읽는다. + 3. ‘내 발화만’을 눌러 학습자 턴만 남는지 확인한 뒤 ‘전체’로 되돌린다. +- 기대결과: 실제보낸턴/응답순서보존 +- side effect: `none` +- 필요 데이터: 순서를 알고 있는 저장 완료 테스트 축어록 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### R03 · 리뷰새로고침 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 저장된 축어록과 리뷰 데이터가 있는 종료 테스트 회기 +- 단계: + 1. ‘축어록’ 탭에서 턴 수와 한 개의 비개인정보 문장을 기록한다. + 2. 브라우저를 새로고침한다. + 3. 동일 리뷰 주소에서 같은 탭을 열어 기록한 턴과 문장이 유지되는지 확인한다. +- 기대결과: 내용지속 +- side effect: `none` +- 필요 데이터: 저장 완료된 종료 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### R04 · 피드백탭 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 리뷰 생성 상태가 알려진 종료 테스트 회기 +- 단계: + 1. ‘피드백’ 탭을 클릭한다. + 2. ‘리뷰 생성 상태’의 완료·대기·실패 표시를 읽는다. + 3. 상태에 맞춰 ‘감정 타임라인 대기’, ‘평가 대기’, 또는 생성된 피드백 카드가 표시되는지 확인한다. +- 기대결과: 완료/대기/실패상태구분 +- side effect: `none` +- 필요 데이터: 평가 상태가 확인된 종료 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### R05 · 자기예측이유공란제출 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 예측 revision이 없는 종료 테스트 회기 +- 단계: + 1. ‘피드백’ 탭에서 ‘첫 자기예측 기록’을 찾는다. + 2. ‘성공 가능성’과 ‘이 예측에 대한 확신’을 조절하되 ‘그렇게 예상한 이유’는 비워 둔다. + 3. ‘첫 예측 기록’ 버튼이 비활성화되어 새 revision이 만들어지지 않는지 확인한다. +- 기대결과: 검증오류/기록생성없음 +- side effect: `none` +- 필요 데이터: 예측 revision이 없는 종료 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### R06 · 자기예측정상저장 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 예측 revision이 없는 종료 테스트 회기 +- 단계: + 1. ‘피드백’ 탭에서 ‘성공 가능성’과 ‘이 예측에 대한 확신’을 선택한다. + 2. ‘그렇게 예상한 이유’에 ‘비개인정보 실습 판단: 질문 뒤 감정을 요약하는 반응을 유지할 수 있습니다.’를 입력한다. + 3. ‘첫 예측 기록’을 한 번 클릭한다. + 4. ‘예측 revision을 기록했어. 외부평가 공개 전 잠금을 완료해줘.’ 상태 메시지를 확인한다. +- 기대결과: 점수/이유1회저장 +- side effect: `test_data_mutation` +- 필요 데이터: 소유자 허용을 받은 미기록 종료 테스트 회기와 비개인정보 판단 이유 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### R07 · 잠그기전수정 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 잠기지 않은 첫 예측이 있는 종료 테스트 회기 +- 단계: + 1. ‘피드백’ 탭에서 ‘성공 가능성’을 다른 5% 단위 값으로 바꾼다. + 2. ‘그렇게 예상한 이유’에 ‘비개인정보 실습 판단: 이전 판단을 더 보수적으로 조정합니다.’를 입력한다. + 3. ‘예측 수정 기록’을 한 번 클릭하고 새 revision 상태 메시지를 확인한다. +- 기대결과: 개정반영 +- side effect: `test_data_mutation` +- 필요 데이터: 소유자 허용을 받은 잠기지 않은 예측과 비개인정보 판단 이유 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### R08 · 자기예측명시잠금 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 잠기지 않은 최신 예측 revision이 있는 종료 테스트 회기 +- 단계: + 1. ‘명시적 잠금’의 ‘잠근 뒤 수정할 수 없음을 확인했어’ 체크박스를 선택한다. + 2. 활성화된 ‘이 예측 잠그기’를 한 번 클릭한다. + 3. ‘잠근 예측은 수정할 수 없어’와 ‘독립 수행 관찰’ 표시를 확인한다. + 4. 잠금 버튼이 다시 쓰기 동작을 만들지 않는지 확인한다. +- 기대결과: 잠금상태/중복잠금없음 +- side effect: `test_data_mutation` +- 필요 데이터: 소유자 허용을 받은 잠기지 않은 예측 revision +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### R09 · 잠금후새로고침 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; R08에서 잠긴 예측이 있는 종료 테스트 회기 +- 단계: + 1. ‘피드백’ 탭의 잠금 표시를 확인한다. + 2. 브라우저를 새로고침한다. + 3. 다시 ‘피드백’ 탭을 열어 ‘예측 수정 기록’ 입력 폼이 없고 잠금 상태가 유지되는지 확인한다. +- 기대결과: 수정금지유지 +- side effect: `none` +- 필요 데이터: 잠긴 예측이 저장된 종료 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### R10 · 외부평가공개순서 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 같은 조건의 잠기지 않은 회기와 잠긴 회기 각 1개 +- 단계: + 1. 잠기지 않은 회기의 ‘피드백’ 탭에서 ‘아직 공개하지 않음’ 또는 ‘예측을 잠가야 비교 가능’을 확인한다. + 2. 잠긴 회기의 같은 카드를 열어 ‘독립 수행 관찰’과 허용된 평가 범위만 표시되는지 확인한다. + 3. 두 회기에서 실제 진단·인구집단 주장이 표시되지 않는지 확인한다. +- 기대결과: 잠금전비공개·잠금후허용범위 +- side effect: `none` +- 필요 데이터: 잠금 전·후 상태가 분리된 종료 테스트 회기 2개 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### R11 · 비식별가짜이름피드백 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; [NAME]만 포함한 비식별 테스트 축어록이 있는 종료 회기 +- 단계: + 1. ‘축어록’ 탭을 연다. + 2. [NAME]이 있던 테스트 문장이 ‘익명 인물은’ 또는 ‘익명 인물이’로 표시되는지 확인한다. + 3. [NAME] 토큰이나 실제 이름이 화면에 남지 않는지 확인한다. +- 기대결과: 상담자/내담자역할치환·실명미노출(실제개인정보금지) +- side effect: `none` +- 필요 데이터: 실제 개인정보 없이 [NAME] 토큰만 가진 테스트 축어록 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### R12 · 워크시트빈필수제출 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 워크시트 자동 초안이 있는 종료 테스트 회기 +- 단계: + 1. ‘워크시트’ 탭에서 ‘사례개념화 워크시트’를 연다. + 2. 현재 소스의 저장 버튼은 빈 필수값 검증이 아니라 변경 여부만 확인함을 확인한다. + 3. 빈 필수 제출용 검증 문구가 없으므로 임의 저장을 하지 않고 BLOCKED로 기록한다. +- 기대결과: 검증안내 +- side effect: `none` +- 필요 데이터: 빈 필수값 검증이 실제로 배포된 테스트 환경 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### R13 · 테스트워크시트작성저장 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 저장되지 않은 워크시트 자동 초안이 있는 종료 테스트 회기 +- 단계: + 1. ‘워크시트’ 탭에서 첫 번째 표시된 워크시트 textarea를 찾는다. + 2. 해당 aria-label이 나타내는 항목에 ‘비개인정보 실습 메모: 감정을 요약하고 개방형 질문을 이어간다.’를 입력한다. + 3. 활성화된 ‘저장’을 한 번 클릭한다. + 4. 상태가 ‘저장된 학습자 제출본’ 또는 ‘저장됨’으로 바뀌는지 확인한다. +- 기대결과: 저장성공 +- side effect: `test_data_mutation` +- 필요 데이터: 소유자 허용을 받은 워크시트 자동 초안과 비개인정보 메모 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### R14 · 워크시트다시열기 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; R13 저장본이 있는 종료 테스트 회기 +- 단계: + 1. ‘워크시트’ 탭에서 저장한 비개인정보 메모를 확인한다. + 2. ‘축어록’ 탭으로 이동한 뒤 다시 ‘워크시트’ 탭을 연다. + 3. 상태가 ‘저장된 학습자 제출본’이고 저장한 문장이 유지되는지 확인한다. +- 기대결과: 내용보존 +- side effect: `none` +- 필요 데이터: 저장 완료된 테스트 워크시트 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### R15 · 피드백OFF 테스트계정 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; learnerFeedbackEnabled=false로 사전 설정된 종료 테스트 계정·회기 +- 단계: + 1. ‘피드백’ 탭을 연다. + 2. ‘먼저 예측하고 잠그는 내 기록만 보존합니다’ 제목을 확인한다. + 3. ‘감정 밸런스 타임라인’, ‘좋았던 순간’, ‘다음 회기 개선점’ 등 파생 피드백 카드가 표시되지 않는지 확인한다. +- 기대결과: 피드백/파생카드차단(사전설정필요) +- side effect: `none` +- 필요 데이터: 피드백 OFF가 확인된 테스트 계정과 종료 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### R16 · 피드백ON 테스트계정 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; learnerFeedbackEnabled=true로 사전 설정된 종료 테스트 계정·회기 +- 단계: + 1. ‘피드백’ 탭을 연다. + 2. ‘먼저 예측하고, 잠근 뒤, 근거로 교정하기’ 제목을 확인한다. + 3. 평가 데이터가 있으면 해당 카드가, 없으면 ‘평가 대기’ 또는 ‘아직 생성된 내담자 피드백이 없습니다.’가 표시되는지 확인한다. +- 기대결과: 정상표시(사전설정필요) +- side effect: `none` +- 필요 데이터: 피드백 ON이 확인된 테스트 계정과 종료 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### R17 · 리뷰에서다음회기준비 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 전이 또는 처방 반환 intent가 있는 종료 테스트 회기 +- 단계: + 1. ‘피드백’ 탭에서 반환 intent의 ‘같은 전이 과제로 다시 연습’ 또는 ‘같은 처방으로 다시 연습’을 찾는다. + 2. 링크의 출처·성공 기준 안내를 확인한다. + 3. 해당 링크를 열어 /learn/practice 주소에 원본 intent 검색값이 유지되는지 확인한다. + 4. 기존 리뷰 주소와 저장된 축어록을 다시 열어 남아 있는지 확인한다. +- 기대결과: 같은사례로이동·기존기록보존 +- side effect: `none` +- 필요 데이터: 검증된 반환 intent가 있는 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### R18 · 비교가능2회리뷰 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 같은 비식별 사례의 종료 테스트 회기 2개 +- 단계: + 1. 첫 번째 /review에서 ‘축어록’과 ‘피드백’의 회기 ID·근거 턴을 기록한다. + 2. 두 번째 /review를 열어 같은 항목을 기록한다. + 3. 각 화면이 자기 주소의 회기 ID와 축어록만 표시하고 근거가 섞이지 않는지 대조한다. +- 기대결과: 각회기근거구분 +- side effect: `none` +- 필요 데이터: 같은 사례에 속한 종료 테스트 회기 2개 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### R19 · 모바일리뷰탭/폼 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 390×844 모바일 뷰포트; 예측 입력 가능한 종료 테스트 회기 +- 단계: + 1. 390×844에서 /learn/session/{테스트회기ID}/review를 연다. + 2. ‘축어록’, ‘피드백’, ‘워크시트’ 탭을 차례로 눌러 각각의 패널이 보이는지 확인한다. + 3. ‘피드백’ 탭에서 ‘성공 가능성’과 ‘그렇게 예상한 이유’ 입력란을 스크롤해 완전히 조작 가능한지 확인한다. + 4. 가로 스크롤이나 화면 밖에 가려진 탭·입력란이 없는지 확인한다. +- 기대결과: 가로넘침/가려진조작없이완료 +- side effect: `none` +- 필요 데이터: 모바일 뷰포트와 예측 입력 가능한 종료 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### R20 · 평가실패회기재시도 + +- actor: `learner_test` +- 사전조건: learner_test 로그인; 실제 배포본; 자연 발생한 session_end 평가 실패가 기록된 테스트 전용 회기 +- 단계: + 1. ‘리뷰 생성 상태’에 실제 평가 실패와 재시도 제어가 보이는지 확인한다. + 2. 실패 원인 안내와 저장된 축어록을 읽는다. + 3. 자연 발생 실패 데이터가 없으면 재평가 요청이나 장애 유발을 하지 않고 BLOCKED로 기록한다. +- 기대결과: 상태안내·기록보존·다음회기막힘없음(실제실패데이터필요) +- side effect: `none` +- 필요 데이터: 자연 발생 session_end 평가 실패가 보존된 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +## H · 이력·설정 + +### H01 · 기록목록이동 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정 +- 단계: + 1. 인증된 학습자 테스트 계정으로 `/learn/history`를 연다. + 2. `회기 기록을 찾고 정리합니다.` 제목과 `회기 기록` 영역을 확인한다. + 3. `회기 기록 목록`의 각 카드에 현재 계정의 페르소나 code·상태·단계·턴 수·날짜가 표시되는지 확인한다. +- 기대결과: 실계정자기기록 +- side effect: `none` +- 필요 데이터: 인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### H02 · 기록검색 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정; 검색 가능한 테스트 페르소나 code 또는 상태 문자열 +- 단계: + 1. `/learn/history`의 `aria-label=기록 검색` 입력에 테스트 페르소나 code 또는 상태 문자열을 입력한다. + 2. 목록 상단의 결과 수와 `회기 기록 목록` 카드를 확인한다. + 3. 표시된 카드의 code·단계·상태 중 적어도 하나가 입력한 검색어와 일치하는지 확인한다. +- 기대결과: 일치회기만 +- side effect: `none` +- 필요 데이터: 검색 가능한 테스트 페르소나 code 또는 상태 문자열 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### H03 · 검색결과없음 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정 +- 단계: + 1. `기록 검색`에 존재하지 않는 임의 문자열을 입력한다. + 2. 빈 상태 문구와 결과 수 0을 확인한다. + 3. 입력 내용을 모두 지운다. + 4. `전체 회기`과 기존 목록이 다시 표시되는지 확인한다. +- 기대결과: 빈상태와검색해제복구 +- side effect: `none` +- 필요 데이터: 인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### H04 · 진행중필터 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정; 진행 중 테스트 회기 +- 단계: + 1. `/learn/history`의 `기록 상태 필터`에서 `진행 중` 버튼을 누른다. + 2. 버튼의 `aria-pressed=true`와 제목 `진행 중`을 확인한다. + 3. 각 결과 카드에 `이어하기`가 있고 종료 카드용 `리뷰`·`기록` 조작이 섞이지 않는지 확인한다. +- 기대결과: 이어가기대상만 +- side effect: `none` +- 필요 데이터: 진행 중 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### H05 · 종료필터 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정; 종료 테스트 회기; 기능확인필요; 기능확인필요 +- 단계: + 1. `/learn/history`에서 종료된 테스트 기록을 먼저 찾는다. + 2. 소스의 상태 필터에는 `종료` 항목이 확인되지 않고 `전체`, `진행 중`, `리뷰 필요`, `보관됨`만 있으므로 실제 배포에서 `종료` 필터가 있는지 확인한다. + 3. `종료` 필터가 있으면 정확한 버튼 라벨·결과 카드 상태를 기록한다. + 4. 없으면 기존 필터를 임의로 대체하지 않고 `기능확인필요`로 기록한다. +- 기대결과: 완료대상만 +- side effect: `none` +- 필요 데이터: 종료 테스트 회기; 기능확인필요 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### H06 · 리뷰필요필터 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정; 리뷰 준비 상태 테스트 회기 +- 단계: + 1. `/learn/history`의 `기록 상태 필터`에서 `리뷰 필요` 버튼을 누른다. + 2. `aria-pressed=true`와 목록 제목 `리뷰 필요`를 확인한다. + 3. 각 결과 카드가 `리뷰` 조작을 제공하고 `리뷰 가능` 상태인지 확인한다. +- 기대결과: 리뷰대상만 +- side effect: `none` +- 필요 데이터: 리뷰 준비 상태 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### H07 · 검색+상태조합 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정 +- 단계: + 1. `기록 검색`에 테스트 페르소나 code를 입력한다. + 2. `기록 상태 필터`에서 `진행 중` 또는 `리뷰 필요`를 선택한다. + 3. 검색어를 유지한 채 결과 카드가 선택한 상태와 code 조건을 모두 만족하는지 확인한다. +- 기대결과: 두조건유지 +- side effect: `none` +- 필요 데이터: 인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### H08 · 목록정렬 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정; 기능확인필요 +- 단계: + 1. `/learn/history`를 열고 카드의 시작 날짜를 위에서 아래로 기록한다. + 2. 소스는 `sessionSortTime` 내림차순 정렬이며 UI 정렬 버튼은 확인되지 않았으므로 실제 배포에 정렬 조작이 있는지 확인한다. + 3. 정렬 조작이 있으면 라벨과 전후 날짜 순서를 기록한다. + 4. 정렬 조작이 없으면 날짜 내림차순 목록만 관찰하고 `기능확인필요`로 기록한다. +- 기대결과: 날짜기준일관 +- side effect: `none` +- 필요 데이터: 인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### H09 · 특정기록열고뒤로 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정 +- 단계: + 1. 검색어 또는 `리뷰 필요` 필터를 적용한 `/learn/history`에서 특정 카드의 `리뷰` 또는 `기록` 버튼을 누른다. + 2. 회기 리뷰의 URL에 해당 session ID가 포함되는지 확인한다. + 3. 브라우저 뒤로가기로 `/learn/history`에 돌아온다. + 4. 검색어·필터·결과 목록 맥락이 복귀 후 유지되는지 확인한다. +- 기대결과: 목록맥락복귀 +- side effect: `none` +- 필요 데이터: 인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### H10 · 테스트종료기록보관 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정; 소유자 허용 뒤 폐기 가능 종료 테스트 회기 +- 단계: + 1. 소유자가 명시적으로 허용한 폐기 가능 종료 테스트 회기만 `/learn/history`에서 찾는다. + 2. 대상 카드의 code·날짜·턴 수를 증거로 기록하고 `보관` 버튼을 확인한다. + 3. 허용이 없으면 버튼을 누르지 않고 safety gate와 `NOT_RUN`을 유지한다. + 4. 허용이 있으면 `보관`을 누르고 `회기를 보관했습니다.`와 카드의 `복원` 전환을 확인한다. + 5. `보관됨` 필터에서 같은 code·날짜·턴 수의 원본 카드가 남아 있는지 확인한다. +- 기대결과: 보관목록이동·원본보존(사용자허용필요) +- side effect: `test_data_mutation_requires_owner_approval` +- 필요 데이터: 소유자 허용 뒤 폐기 가능 종료 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### H11 · 보관기록복원 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정; 소유자 허용 뒤 보관된 폐기 가능 테스트 회기 +- 단계: + 1. 소유자가 명시적으로 허용한 보관된 폐기 가능 테스트 회기만 `보관됨` 필터에서 찾는다. + 2. 대상 카드의 code·날짜·턴 수를 기록하고 `복원` 버튼을 확인한다. + 3. 허용이 없으면 버튼을 누르지 않고 safety gate와 `NOT_RUN`을 유지한다. + 4. 허용이 있으면 `복원`을 누르고 `보관을 해제했습니다.`를 확인한다. + 5. `전체 회기` 또는 원래 상태 필터에서 같은 원본 카드가 다시 보이는지 확인한다. +- 기대결과: 원래목록복귀 +- side effect: `test_data_mutation_requires_owner_approval` +- 필요 데이터: 소유자 허용 뒤 보관된 폐기 가능 테스트 회기 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### H12 · 설정화면조회 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정 +- 단계: + 1. 인증된 학습자 테스트 계정으로 `/settings`를 연다. + 2. 설정 레일의 `계정` 버튼을 선택한다. + 3. `계정 요약`과 `표시 이름`, `이메일`, `소속` 필드를 확인한다. + 4. 이메일 입력이 비활성화된 로그인 계정 정보인지 확인한다. +- 기대결과: 자기계정정보 +- side effect: `none` +- 필요 데이터: 인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### H13 · 테스트계정프로필수정후복원 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정; 소유자 허용 뒤 원래 값을 복원할 수 있는 테스트 계정 +- 단계: + 1. 소유자가 허용한 테스트 계정으로 `/settings`의 `계정` 섹션을 연다. + 2. `표시 이름`과 `소속`의 원래 값을 증거로 기록한다. + 3. 한 필드만 비식별 테스트 값으로 바꾸고 `저장`을 누른 뒤 `저장됨`을 확인한다. + 4. 새로고침 후 같은 테스트 값이 재조회되는지 확인한다. + 5. 원래 값을 다시 입력해 `저장`하고 새로고침하여 원복을 확인한다. +- 기대결과: 저장/재조회일관(기존값보존) +- side effect: `test_data_mutation_requires_owner_approval` +- 필요 데이터: 소유자 허용 뒤 원래 값을 복원할 수 있는 테스트 계정 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### H14 · 설정변경취소 + +- actor: `learner_test` +- 사전조건: 인증된 학습자 테스트 계정 +- 단계: + 1. 인증된 학습자 테스트 계정으로 `/settings`의 `계정` 섹션을 연다. + 2. `표시 이름` 또는 `소속`에 저장하지 않을 임시 값을 입력한다. + 3. 사이드 레일의 다른 설정 섹션(예: `테마`)을 선택한 뒤 `계정`으로 돌아온다. + 4. 저장하지 않은 임시 값이 영속화되지 않았고 원래 값이 다시 표시되는지 확인한다. +- 기대결과: 기존값보존 +- side effect: `none` +- 필요 데이터: 인증된 테스트 계정; 실제 배포 데이터는 읽기 전용으로 관찰 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +## T · 교수자 + +### T01 · 교수자로그인홈 + +- actor: `teacher_test` +- 사전조건: 승인된 teacher_test 계정으로 로그인 완료; 실제 배포본 https://vignette.chanpaca.net; 비식별 테스트 회기만 조회 +- 단계: + 1. 로그인된 teacher_test 세션에서 /teach 를 연다. + 2. 상단 역할 표기가 ‘교수자’이고 제목이 ‘교수 콘솔’인지 확인한다. + 3. ‘학생 분석’과 ‘새로고침’ 버튼이 표시되는지 확인한다. + 4. 주소가 /teach 이고 관리자 편집 제어가 보이지 않는지 기록한다. +- 기대결과: 허용콘솔 +- side effect: `auth_session` +- 필요 데이터: teacher_test 계정 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### T02 · 학습자권한교수주소시도 + +- actor: `learner_test` +- 사전조건: 인증된 learner_test 역할의 실제 브라우저 세션; admin 역할·admin 공간 전환으로 대체 검증 금지 +- 단계: + 1. teacher_test 세션에서 /admin/users 주소를 직접 연다. + 2. 관리자 사용자 목록이나 ‘가입 승인과 권한 관리’가 표시되지 않는지 확인한다. + 3. 권한 오류, 차단 화면 또는 허용된 교수자 경로로의 전환을 확인한다. + 4. 주소와 화면의 접근 차단 상태를 기록한다. +- 기대결과: 차단 +- side effect: `none` +- 필요 데이터: learner_test 계정; 관리자 계정은 접근거부 PASS를 대체하지 않음 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### T03 · 대기목록에서테스트학습자리뷰 + +- actor: `teacher_test` +- 사전조건: 승인된 teacher_test 로그인 완료; 비식별 test_learner와 종료·review_ready 테스트 회기 1건이 교수자 큐에 존재 +- 단계: + 1. /teach 에서 test_learner가 포함된 최근 회기 행을 찾는다. + 2. 그 행의 ‘상세 리뷰’ 동작을 선택한다. + 3. 주소가 /teach/session/{test_session_id}/review 이고 리뷰 헤더의 학습자·세션이 선택 행과 일치하는지 확인한다. + 4. 리뷰 생성 상태가 ‘준비됨’인지 확인한다. +- 기대결과: 대상일치 +- side effect: `none` +- 필요 데이터: test_learner 표시명, 종료·review_ready 테스트 session_id +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### T04 · 종료상태와리뷰상태구분 + +- actor: `teacher_test` +- 사전조건: 승인된 teacher_test 로그인 완료; 종료된 test_learner 회기 2건(검토 대기 1건, 검토 완료 또는 메모 저장 1건) 존재 +- 단계: + 1. /teach 에서 두 테스트 회기의 종료 상태와 검토 배지를 확인한다. + 2. 검토 대기 행과 검토 완료 또는 메모 저장 행을 각각 연다. + 3. 각 상세 리뷰의 세션 종료 표시와 교수자 검토 상태가 행의 배지와 일치하는지 확인한다. + 4. 종료 여부와 검토 여부가 하나의 같은 상태로 합쳐져 표시되지 않는지 기록한다. +- 기대결과: 대기/완료명확 +- side effect: `none` +- 필요 데이터: 상태가 다른 비식별 종료 테스트 회기 2건 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### T05 · 학습자검색 + +- actor: `teacher_test` +- 사전조건: 승인된 teacher_test 로그인 완료; test_learner의 표시명 또는 비식별 ID가 학생 분석 목록에 존재 +- 단계: + 1. /teach/analysis 에서 ‘학습자 검색’ 입력칸을 찾는다. + 2. test_learner의 고유 검색 문자열을 입력한다. + 3. 검색 가능한 학습자 추이 테이블에 해당 학습자만 남는지 확인한다. + 4. 남은 행의 표시명 또는 ID가 입력한 테스트 대상과 일치하는지 확인한다. +- 기대결과: 일치대상 +- side effect: `none` +- 필요 데이터: test_learner 표시명 또는 비식별 ID +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### T06 · 미일치검색복구 + +- actor: `teacher_test` +- 사전조건: 승인된 teacher_test 로그인 완료; 학생 분석 목록에 test_learner 존재 +- 단계: + 1. /teach/analysis 의 ‘학습자 검색’에 존재하지 않는 비식별 문자열을 입력한다. + 2. ‘검색 결과 없음’ 빈 상태와 검색어를 줄이라는 안내를 확인한다. + 3. 입력값을 모두 지운다. + 4. test_learner 행이 다시 표시되고 이전 빈 상태가 사라지는지 확인한다. +- 기대결과: 빈상태/해제정상 +- side effect: `none` +- 필요 데이터: 존재하지 않는 비식별 검색 문자열, test_learner +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### T07 · 최신활동정렬 + +- actor: `teacher_test` +- 사전조건: 승인된 teacher_test 로그인 완료; 서로 다른 최근 활동 시각을 가진 비식별 학습자 2명 이상 존재 +- 단계: + 1. /teach/analysis 의 ‘학습자 정렬’에서 최신 활동 정렬 제어를 선택한다. + 2. 목록 첫 두 행의 최근 활동 시각 또는 같은 기준으로 표시된 최신 회기를 확인한다. + 3. 첫 행이 둘째 행보다 오래되지 않았는지 확인한다. + 4. 정렬 전후 선택한 학습자 검색 조건이 유지되는지 확인한다. +- 기대결과: 목록정렬일관 +- side effect: `none` +- 필요 데이터: 최근 활동 시각이 다른 test_learner 2명 이상 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### T08 · 회기수정렬 + +- actor: `teacher_test` +- 사전조건: 승인된 teacher_test 로그인 완료; 완료 회기 수가 다른 비식별 학습자 2명 이상 존재 +- 단계: + 1. /teach/analysis 의 ‘학습자 정렬’에서 회기 수 정렬 제어를 선택한다. + 2. 표의 ‘회기’ 열에서 첫 두 행의 수치를 확인한다. + 3. 선택한 정렬 방향과 수치 순서가 일치하는지 확인한다. + 4. 행을 새로 열지 않아도 정렬이 화면 값만 바꾸고 데이터를 수정하지 않는지 확인한다. +- 기대결과: 값과순서일치 +- side effect: `none` +- 필요 데이터: 완료 회기 수가 다른 test_learner 2명 이상 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### T09 · 학습자요약펼침닫힘 + +- actor: `teacher_test` +- 사전조건: 승인된 teacher_test 로그인 완료; 학생 분석 목록에 비식별 학습자 2명 이상 존재 +- 단계: + 1. /teach/analysis 에서 첫 test_learner 행의 ‘요약 펼치기’ 버튼을 선택한다. + 2. 그 행에만 회기·점수·추이 요약이 열리는지 확인한다. + 3. 같은 행의 ‘요약 접기’를 선택한다. + 4. 둘째 행의 펼침 상태와 표시 값이 바뀌지 않았는지 확인한다. +- 기대결과: 다른행상태오염없음 +- side effect: `none` +- 필요 데이터: test_learner 2명 이상 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### T10 · 상세분석진입복귀 + +- actor: `teacher_test` +- 사전조건: 승인된 teacher_test 로그인 완료; test_learner가 학생 분석 목록에 존재 +- 단계: + 1. /teach/analysis 에서 test_learner 행의 ‘상세 보기’를 선택한다. + 2. 상세 제목의 학습자 표시명이 선택한 행과 같은지 확인한다. + 3. 상단 ‘콘솔로’ 버튼을 선택한다. + 4. /teach 로 돌아오고 로그인 역할이 교수자로 유지되는지 확인한다. +- 기대결과: 같은학습자맥락 +- side effect: `none` +- 필요 데이터: test_learner +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### T11 · 분석탭전환 + +- actor: `teacher_test` +- 사전조건: 승인된 teacher_test 로그인 완료; 상세 분석에 들어갈 test_learner 존재 +- 단계: + 1. test_learner의 상세 분석을 연다. + 2. ‘학습자 상세 분석 탭’에서 ‘페르소나별 회기’, ‘회기별 추이’, ‘전체 회기’, ‘단계 분포’를 차례로 선택한다. + 3. 각 탭에서 선택된 탭만 활성 상태이고 해당 섹션 제목 또는 빈 상태가 보이는지 확인한다. + 4. 탭 전환 뒤 학습자 이름과 분석 요약 수치가 동일 대상의 값으로 유지되는지 확인한다. +- 기대결과: 선택에맞는데이터 +- side effect: `none` +- 필요 데이터: test_learner +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### T12 · 페르소나별기록그룹열기 + +- actor: `teacher_test` +- 사전조건: 승인된 teacher_test 로그인 완료; test_learner에 특정 페르소나 완료 회기 1건 이상 존재 +- 단계: + 1. test_learner 상세의 ‘페르소나별 회기’ 탭을 연다. + 2. 대상 페르소나의 ‘회기 펼치기’ 버튼을 선택한다. + 3. 펼친 그룹의 회기 수와 대상 session_id 또는 회기 번호가 사전 설정 자료와 일치하는지 확인한다. + 4. 다른 페르소나 그룹을 열지 않은 상태에서 대상 그룹만 확장되는지 확인한다. +- 기대결과: 해당사례기록 +- side effect: `none` +- 필요 데이터: test_learner, 페르소나 코드, 해당 테스트 session_id +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### T13 · 회기타임라인개별리뷰 + +- actor: `teacher_test` +- 사전조건: 승인된 teacher_test 로그인 완료; test_learner의 종료·review_ready 테스트 session_id 존재 +- 단계: + 1. test_learner 상세의 ‘전체 회기’ 탭을 연다. + 2. 대상 회기 번호의 ‘상세 리뷰’ 또는 ‘진행 기록’ 버튼을 선택한다. + 3. 주소의 session_id와 타임라인에서 선택한 test_session_id가 일치하는지 확인한다. + 4. 종료 회기라면 리뷰 화면의 축어록 또는 리뷰 준비 상태가 표시되는지 확인한다. +- 기대결과: 정확한회기 +- side effect: `none` +- 필요 데이터: test_learner, 종료·review_ready test_session_id +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### T14 · 추이차트부족데이터 + +- actor: `teacher_test` +- 사전조건: 승인된 teacher_test 로그인 완료; 평가 점수 또는 추이 표본이 부족한 test_learner 존재 +- 단계: + 1. 표본 부족 test_learner의 상세 분석에서 ‘회기별 추이’를 연다. + 2. 차트가 값 없는 항목을 ‘저장된 점수 없음’ 또는 중립 빈 상태로 표시하는지 확인한다. + 3. ‘상승’·‘하락’ 같은 근거 없는 추이 단정이 표시되지 않는지 확인한다. + 4. 다른 탭의 회기 수와 차트에 표시된 표본 수가 모순되지 않는지 확인한다. +- 기대결과: 무근거성과단정없음 +- side effect: `none` +- 필요 데이터: 평가 표본 부족 test_learner +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### T15 · 테스트학습자검토메모임시작성취소 + +- actor: `teacher_test` +- 사전조건: 승인된 teacher_test 로그인 완료; 종료·review_ready test_session_id 존재 +- 단계: + 1. 대상 /teach/session/{test_session_id}/review 의 교수자 검토 영역을 연다. + 2. ‘교수자 검토 메모’에 ‘CUA 임시 메모 - 저장 금지’를 입력한다. + 3. ‘메모 저장’을 누르지 않고 ‘전체 회기’ 탭으로 이동한 뒤 같은 리뷰를 다시 연다. + 4. 임시 문자열과 저장 시각이 남지 않았는지 확인한다. +- 기대결과: 저장안됨 +- side effect: `none` +- 필요 데이터: 비식별 종료 테스트 회기 1건 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### T16 · 테스트워크시트검토저장 + +- actor: `teacher_test` +- 사전조건: 승인된 teacher_test 로그인 완료; 소유자가 허용한 비식별 test_learner의 저장된 워크시트와 종료·review_ready test_session_id 존재; 테스트 뒤 정리 가능한 테스트 자료만 사용 +- 단계: + 1. 대상 /teach/session/{test_session_id}/review 에서 ‘워크시트 검수’를 연다. + 2. ‘워크시트 검수 메모’에 비식별 테스트 문자열을 입력한다. + 3. ‘수정요청’을 실제로 선택하고 저장 완료 상태 또는 검수 시각이 표시될 때까지 기다린다. + 4. 같은 URL을 새로고침하거나 목록에서 다시 열어 메모·수정요청 상태·검수 시각이 같은 test_session_id에만 남았는지 재조회한다. + 5. 다른 테스트 회기의 워크시트 상태가 바뀌지 않았는지 확인한다. +- 기대결과: 해당테스트항목만저장(승인필요) +- side effect: `test_data_mutation` +- 필요 데이터: 소유자 허용 비식별 워크시트 test_session_id, 저장된 학습자 워크시트 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### T17 · 안전큐대상열기 + +- actor: `teacher_test` +- 사전조건: 승인된 teacher_test 로그인 완료; 비식별 test_learner의 사전 설정 안전 알림 테스트 회기 존재 +- 단계: + 1. /teach 의 ‘위기 알림’ 큐에서 비식별 테스트 항목만 찾는다. + 2. 항목의 학습자 라벨·페르소나 코드·위험도·session_id 앞부분이 사전 설정한 테스트 자료와 일치하는지 확인한다. + 3. 큐가 원문 축어록이나 실제 연락처를 노출하지 않고 안내 번호·리소스 제목만 표시하는지 확인한다. + 4. 알림 상태를 승인·해결로 변경하지 않고 화면을 닫는다. +- 기대결과: 해당테스트위기회기·안내(실제학생정보최소화) +- side effect: `none` +- 필요 데이터: 비식별 안전 알림 test_session_id +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### T18 · 감독·연구화면 + +- actor: `teacher_test` +- 사전조건: 승인된 teacher_test 로그인 완료; 실제 배포본 https://vignette.chanpaca.net +- 단계: + 1. 교수자 사이드바에서 ‘감독·연구’를 선택하거나 /teach/supervision 을 연다. + 2. 감독 우선순위와 증거 매니페스트 영역이 표시되는지 확인한다. + 3. 관리자 전용 지속 개선 승인 원장이나 사용자 권한 편집 제어가 표시되지 않는지 확인한다. + 4. 교수자 역할 표기와 주소가 유지되는지 기록한다. +- 기대결과: 권한내기능표시 +- side effect: `none` +- 필요 데이터: teacher_test 계정 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### T19 · 모바일교수자목록검색리뷰 + +- actor: `teacher_test` +- 사전조건: 승인된 teacher_test 로그인 완료; 390px 폭; test_learner와 종료·review_ready test_session_id 존재 +- 단계: + 1. 390px 폭에서 /teach/analysis 를 열고 ‘학습자 검색’에 test_learner를 입력한다. + 2. 가로 스크롤 없이 대상 행의 ‘상세 보기’를 선택한다. + 3. ‘전체 회기’ 탭에서 대상 session_id의 상세 리뷰를 연다. + 4. 리뷰 헤더·탭·뒤로가기 동작이 화면 밖으로 잘리지 않는지 확인한다. +- 기대결과: 핵심동선조작가능 +- side effect: `none` +- 필요 데이터: test_learner, test_session_id +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### T20 · 교수자홈버튼 + +- actor: `teacher_test` +- 사전조건: 승인된 teacher_test 로그인 완료; 실제 배포본 https://vignette.chanpaca.net +- 단계: + 1. /teach/analysis 를 열어 ‘학생 분석’ 화면인지 확인한다. + 2. 헤더의 ‘콘솔로’ 버튼을 선택한다. + 3. 주소가 정확히 /teach 로 바뀌고 제목이 ‘교수 콘솔’로 바뀌는지 확인한다. + 4. 경로가 유지되거나 잘못된 화면으로 이동하면 URL·화면 제목을 불일치 증거로 기록한다. +- 기대결과: 정상/teach복귀(경로불일치후보확인) +- side effect: `none` +- 필요 데이터: teacher_test 계정 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +## O · 관리자 + +### O01 · 관리자로그인홈 + +- actor: `admin_test` +- 사전조건: 승인된 admin_test 계정으로 로그인 완료; 실제 배포본 https://vignette.chanpaca.net +- 단계: + 1. 로그인된 admin_test 세션에서 /admin 을 연다. + 2. 역할 표기가 ‘관리자’이고 제목이 ‘현재 서비스 상태’인지 확인한다. + 3. ‘새로고침’과 운영 요약이 표시되는지 확인한다. + 4. 사용자 변경·운영 설정 저장 버튼은 누르지 않는다. +- 기대결과: 권한내콘솔 +- side effect: `auth_session` +- 필요 데이터: admin_test 계정 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### O02 · 학습자관리주소 + +- actor: `learner_test` +- 사전조건: 인증된 learner_test 역할의 실제 브라우저 세션; admin 역할·admin 공간 전환으로 대체 검증 금지 +- 단계: + 1. admin_test 세션에서 /learn 주소를 직접 연다. + 2. 학습자 홈의 학습 시작 제어와 개인 회기 목록이 관리자에게 표시되지 않는지 확인한다. + 3. 권한 오류, 차단 화면 또는 허용된 관리자 경로 전환을 확인한다. + 4. 주소와 접근 차단 상태를 기록한다. +- 기대결과: 접근차단 +- side effect: `none` +- 필요 데이터: learner_test 계정; 관리자 계정은 접근거부 PASS를 대체하지 않음 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### O03 · 운영홈서비스상태읽기 + +- actor: `admin_test` +- 사전조건: 승인된 admin_test 로그인 완료; 실제 배포본 https://vignette.chanpaca.net +- 단계: + 1. /admin 의 ‘현재 서비스 상태’에서 상태 문구·환경·엔진 모드·갱신 시각을 읽는다. + 2. ‘새로고침’을 한 번 선택하고 완료 뒤 갱신 시각이 표시되는지 확인한다. + 3. 운영 요약의 활성 회기·온라인 사용자 수가 숫자 또는 명시적 빈 값으로 표시되는지 확인한다. + 4. 상태 값을 변경하거나 진단 조치를 실행하지 않는다. +- 기대결과: 실제상태/갱신시각명확 +- side effect: `none` +- 필요 데이터: admin_test 계정 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### O04 · AI운영기간변경 + +- actor: `admin_test` +- 사전조건: 승인된 admin_test 로그인 완료; 실제 배포본 https://vignette.chanpaca.net +- 단계: + 1. /admin/ai 의 ‘계량 기간’에서 현재 선택값을 기록한다. + 2. 표시된 다른 기간 버튼을 선택한다. + 3. ‘일별 비용 흐름’ 차트의 aria-label과 기간 표시가 선택한 일수로 바뀌는지 확인한다. + 4. ‘최근 집계’ 시각과 모델별 사용 원장이 새 기간 기준으로 다시 표시되는지 확인한다. +- 기대결과: 집계기간반영 +- side effect: `none` +- 필요 데이터: admin_test 계정 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### O05 · 모델별사용원장읽기 + +- actor: `admin_test` +- 사전조건: 승인된 admin_test 로그인 완료; 선택 기간에 비식별 사용 원장 또는 명시적 빈 상태 존재 +- 단계: + 1. /admin/ai 에서 ‘AI 사용 핵심 지표’와 모델별 사용 원장 영역을 연다. + 2. 모델명, 호출 수, 토큰 또는 비용 단위, 비용 기준 라벨이 함께 표시되는지 확인한다. + 3. 미계량 호출이 있으면 복원하지 않고 미계량이라는 설명이 표시되는지 확인한다. + 4. 원문 프롬프트·응답·개인 식별 정보가 표에 보이지 않는지 확인한다. +- 기대결과: 모델/계량단위명확 +- side effect: `none` +- 필요 데이터: admin_test 계정 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### O06 · AI설정열고취소 + +- actor: `admin_test` +- 사전조건: 승인된 admin_test 로그인 완료; 현재 AI 엔진 설정이 조회 가능 +- 단계: + 1. /admin/ai 의 ‘AI 운영 설정’에서 현재 공급자·연결 주소·기본 모델·추론 강도를 기록한다. + 2. 저장 제어를 누르지 않고 모델 목록 또는 설정 영역만 연다. + 3. ‘운영 설정 저장’을 선택하지 않은 채 /admin 으로 이동한다. + 4. 다시 /admin/ai 를 열어 기록한 설정 값이 변하지 않았는지 확인한다. +- 기대결과: 운영설정변경없음 +- side effect: `none` +- 필요 데이터: admin_test 계정, 현재 표시된 엔진 모드·모델 값 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### O07 · 사용자검색 + +- actor: `admin_test` +- 사전조건: 승인된 admin_test 로그인 완료; 비식별 test_learner 계정이 사용자 목록에 존재 +- 단계: + 1. /admin/users 에서 ‘사용자 목록’ 탭을 선택한다. + 2. ‘사용자 검색’에 test_learner의 고유 비식별 검색 문자열을 입력한다. + 3. 남은 행의 표시 이름·역할이 사전 설정한 테스트 계정과 일치하는지 확인한다. + 4. 행의 저장·승인·역할 선택 제어는 누르지 않는다. +- 기대결과: 대상일치 +- side effect: `none` +- 필요 데이터: test_learner 표시명 또는 비식별 이메일 식별자 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### O08 · 사용자검색없음 + +- actor: `admin_test` +- 사전조건: 승인된 admin_test 로그인 완료; 사용자 목록 조회 가능 +- 단계: + 1. /admin/users 의 ‘사용자 목록’ 탭에서 ‘사용자 검색’에 존재하지 않는 문자열을 입력한다. + 2. 검색 조건에 맞는 사용자가 없다는 빈 상태가 표시되는지 확인한다. + 3. 검색어를 지운다. + 4. 목록이 돌아오고 사용자 편집값이 바뀌지 않았는지 확인한다. +- 기대결과: 빈상태/복귀 +- side effect: `none` +- 필요 데이터: 존재하지 않는 비식별 검색 문자열 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### O09 · 사용자정렬 + +- actor: `admin_test` +- 사전조건: 승인된 admin_test 로그인 완료; 사용자 목록에 비식별 계정 2개 이상 존재 +- 단계: + 1. /admin/users 의 ‘사용자 목록’에서 정렬 가능한 열 머리글을 선택한다. + 2. 첫 두 행의 해당 열 값을 기록한다. + 3. 같은 열 머리글을 다시 선택해 정렬 방향을 바꾼다. + 4. 방향 표시와 두 행의 값 순서가 일치하고 사용자 편집 제어가 실행되지 않았는지 확인한다. +- 기대결과: 값/순서일관 +- side effect: `none` +- 필요 데이터: 비식별 사용자 2개 이상 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### O10 · 테스트사용자상세 + +- actor: `admin_test` +- 사전조건: 승인된 admin_test 로그인 완료; 비식별 test_learner 계정이 사용자 목록에 존재 +- 단계: + 1. /admin/users 의 ‘사용자 목록’에서 test_learner를 검색한다. + 2. 해당 행을 펼쳐 표시 이름, 역할, 승인 상태, ‘학습자 AI 피드백’ 값을 확인한다. + 3. 표시된 역할·피드백 상태가 사전 설정된 테스트 값과 일치하는지 확인한다. + 4. select·switch·저장 버튼을 조작하지 않고 행을 다시 접는다. +- 기대결과: 역할/피드백상태확인 +- side effect: `none` +- 필요 데이터: test_learner 계정의 사전 설정 역할·AI 피드백 값 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### O11 · 권한정책화면 + +- actor: `admin_test` +- 사전조건: 승인된 admin_test 로그인 완료; 실제 배포본 https://vignette.chanpaca.net +- 단계: + 1. /admin/access 에서 ‘역할’ 탭을 연다. + 2. 관리자·교수자·학습자 정책 카드와 권한 수가 표시되는지 확인한다. + 3. ‘권한 매트릭스’ 탭을 선택해 리소스별 세 역할 열이 표시되는지 확인한다. + 4. 화면이 정책 표시 전용이며 역할·그룹 저장 API가 분리되지 않았다는 안내를 확인한다. +- 기대결과: 접근정책표시 +- side effect: `none` +- 필요 데이터: admin_test 계정 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### O12 · 프로토콜메뉴접근 + +- actor: `admin_test` +- 사전조건: 승인된 admin_test 로그인 완료; 프로토콜을 생성하지 않는 읽기 전용 확인 +- 단계: + 1. /admin/access 에서 aria-label ‘접근 권한 탭’의 ‘상담 프로토콜’을 선택한다. + 2. ‘상담 프로토콜 초안 등록’ 제목과 ‘등록 프로토콜’ 목록이 함께 표시되는지 확인한다. + 3. ‘프로토콜 제목’, ‘프로토콜 출처’, ‘프로토콜 버전’, ‘프로토콜 라이선스’, ‘프로토콜 원문’ 입력 제어를 확인한다. + 4. 초안 등록·활성화·퇴역 버튼은 누르지 않는다. +- 기대결과: 목록/신규폼존재 +- side effect: `none` +- 필요 데이터: admin_test 계정 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### O13 · 프로토콜검색상태필터 + +- actor: `admin_test` +- 사전조건: 승인된 admin_test 로그인 완료; 제목 또는 상태가 알려진 비식별 테스트 프로토콜 존재 +- 단계: + 1. /admin/access 의 ‘상담 프로토콜’ 탭에서 ‘프로토콜 검색’에 테스트 제목 조각을 입력한다. + 2. ‘프로토콜 상태 필터’를 사전 설정 상태로 선택한다. + 3. 남은 카드의 제목과 상태 배지가 둘 다 기대값과 일치하는지 확인한다. + 4. 검색어와 필터를 원래 값으로 되돌리고 목록이 복구되는지 확인한다. +- 기대결과: 일치항목 +- side effect: `none` +- 필요 데이터: 비식별 테스트 프로토콜 제목 조각과 사전 설정 상태 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### O14 · 프로토콜빈필수초안등록 + +- actor: `admin_test` +- 사전조건: 승인된 admin_test 로그인 완료; 프로토콜 목록의 시작 개수 또는 대상 제목 부재를 기록 +- 단계: + 1. /admin/access 의 ‘상담 프로토콜’ 탭에서 필수 입력을 모두 빈 값으로 둔다. + 2. ‘초안 등록’을 한 번 선택한다. + 3. ‘제목, 출처, 원문을 모두 입력해야 합니다.’ 오류가 표시되는지 확인한다. + 4. 등록 프로토콜 목록에 새 카드가 생기지 않았는지 확인한다. +- 기대결과: 오류안내·생성없음 +- side effect: `none` +- 필요 데이터: admin_test 계정 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### O15 · 프로토콜초안작성취소 + +- actor: `admin_test` +- 사전조건: 승인된 admin_test 로그인 완료; 프로토콜 목록과 기존 검색 결과를 기록 +- 단계: + 1. ‘상담 프로토콜 초안 등록’에 비식별 임시 제목·출처·원문을 입력한다. + 2. ‘초안 등록’을 누르지 않고 ‘역할’ 탭으로 이동한다. + 3. 다시 ‘상담 프로토콜’ 탭을 열어 임시 입력이 새 카드로 생성되지 않았는지 확인한다. + 4. 기존 프로토콜 카드 수와 검색 결과가 시작 기록과 같은지 확인한다. +- 기대결과: 기존자료영향없음 +- side effect: `none` +- 필요 데이터: 비식별 임시 제목·출처·원문 문자열 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### O16 · 티켓복합필터 + +- actor: `admin_test` +- 사전조건: 승인된 admin_test 로그인 완료; 비식별 테스트 티켓이 사전 설정된 상태·카테고리·우선순위로 존재 +- 단계: + 1. /admin/tickets 에서 ‘티켓 검색’에 테스트 제목 조각을 입력한다. + 2. ‘상태 필터’, ‘카테고리 필터’, ‘우선순위 필터’를 사전 설정값으로 선택한다. + 3. 미해결 운영 티켓 큐에 남은 카드가 목표 티켓과 일치하는지 확인한다. + 4. 티켓 상태 변경·해결 노트 저장 제어는 누르지 않는다. +- 기대결과: 일치티켓목록 +- side effect: `none` +- 필요 데이터: 비식별 테스트 티켓의 제목 조각·상태·카테고리·우선순위 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### O17 · 티켓상세열고복귀 + +- actor: `admin_test` +- 사전조건: 승인된 admin_test 로그인 완료; O16의 복합 필터로 남는 비식별 테스트 티켓 존재 +- 단계: + 1. /admin/tickets 에서 O16과 같은 검색·필터를 적용한다. + 2. 남은 테스트 티켓의 상세 열기 제어를 선택한다. + 3. 제목·상태·카테고리가 선택한 카드와 일치하는지 확인한다. + 4. 뒤로 또는 목록 복귀 후 검색어와 필터, 남은 카드가 유지되는지 확인한다. +- 기대결과: 필터맥락유지 +- side effect: `none` +- 필요 데이터: 비식별 테스트 티켓 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### O18 · 개선원장상태조회 + +- actor: `admin_test` +- 사전조건: 승인된 admin_test 로그인 완료; 실제 배포본 https://vignette.chanpaca.net +- 단계: + 1. /admin/continuous-improvement 를 열고 ‘승격보다 근거를 먼저 본다’ 헤더를 확인한다. + 2. 운영 경계, 콘텐츠 검증 단계, 게이트 필수 증거, 수명주기 원장을 차례로 확인한다. + 3. 후보의 증거 상태와 사람 승인 이후 효과가 다른 영역으로 분리되어 있는지 확인한다. + 4. ‘원장 새로고침’ 외의 승인·보류·반려 제어는 누르지 않는다. +- 기대결과: 후보/증거/실행결정구분 +- side effect: `none` +- 필요 데이터: admin_test 계정 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### O19 · 증거부족후보승인시도 + +- actor: `admin_test` +- 사전조건: 승인된 admin_test 로그인 완료; 필수 증거가 부족한 비식별 후보가 지속 개선 원장에 존재 +- 단계: + 1. /admin/continuous-improvement 에서 증거 부족 후보의 ‘게이트 필수 증거’ 상태를 확인한다. + 2. ‘필수 증거 4종 미완료로 승인 차단’ 또는 해당 차단 사유를 확인한다. + 3. 그 후보의 승인 버튼이 disabled 상태인지 접근성 상태와 화면으로 확인한다. + 4. 승인·보류·반려 선택이나 제출 버튼은 어떤 것도 누르지 않고 차단 관찰만 기록한다. +- 기대결과: 승인차단(실제승인제출금지) +- side effect: `none` +- 필요 데이터: 증거 부족 비식별 후보 +- baseline 상태: `NOT_RUN` +- evidence: `[]` + +### O20 · 모바일관리자검색상세 + +- actor: `admin_test` +- 사전조건: 승인된 admin_test 로그인 완료; 390px 폭; 비식별 test_learner 계정 존재 +- 단계: + 1. 390px 폭에서 /admin/users 의 ‘사용자 목록’ 탭을 연다. + 2. ‘사용자 검색’에 test_learner를 입력한다. + 3. 가로 스크롤 없이 대상 행의 상세를 열어 역할·승인 상태·학습자 AI 피드백 상태를 확인한다. + 4. 저장·승인·역할 변경 없이 상세를 닫고, 입력·필터·제어가 화면 밖으로 잘리지 않는지 확인한다. +- 기대결과: 가로넘침없이핵심조작 +- side effect: `none` +- 필요 데이터: test_learner 표시명 또는 비식별 이메일 식별자 +- baseline 상태: `NOT_RUN` +- evidence: `[]` diff --git a/docs/qa/production-computer-use-report-2026-09-09.md b/docs/qa/production-computer-use-report-2026-09-09.md new file mode 100644 index 0000000..08a6356 --- /dev/null +++ b/docs/qa/production-computer-use-report-2026-09-09.md @@ -0,0 +1,35 @@ +# 실배포 Computer Use 128 실행 보고 + +> 기준: 2026-09-09 KST. 이 문서는 실제 배포 CUA 관찰과 local 수정 검증을 분리한다. 128개 카탈로그 생성은 완료됐지만, 128개 전체의 실제 배포 검증 완료를 뜻하지 않는다. 최신 ledger는 PASS 83, RED 2, BLOCKED 10, NOT_RUN 33이다. + +## 참조와 현재 범위 + +- [실행 카탈로그](./production-computer-use-128-2026-09-09.md)와 [기계 판독 카탈로그](./production-computer-use-128-2026-09-09.json)는 시나리오·역할·mutation 경계를 소유한다. +- [실행 ledger](../../outputs/production-cu-2026-09-09/ledger.json)는 실제 상태와 실행 증거를 보존한다. +- 실제 관리자와 일반 학습자 로그인을 확인했다. 일반 학습자는 `/admin`·`/teach` 접근 때 `/learn`으로 차단됐고 P4 추천이 일치했다. 순수 teacher 계정, 독립 관찰자 공개 검증, OFF 계정, 교수자 쓰기와 모바일 실행은 아직 확인하지 않았다. +- 허용된 관리자 P16 회기 1개와 학습자 P4 회기 1개를 사용해 총 2/2를 종료했다. P4 회기는 2분 19초 동안 6턴·실제 AI 3왕복으로 종료했다. 두 회기의 대화·리뷰·워크시트 권한은 기존 승인 범위에서 유지한다. + +## 일반 학습자 실배포 관찰 + +- 새 회기는 문서 재로드 뒤에도 복원됐다. 리뷰 평가는 대기에서 완료로 전환됐다. +- 자기예측은 빈 입력을 차단했고 revision 1→revision 2→명시 잠금 뒤 reload에서도 같은 잠금 상태를 유지했다. +- 워크시트 주호소 수정은 저장 완료 뒤 reload에서 복원됐다. +- 원장에 추가된 `익명 인물입니다` 관찰의 production 원인은 미확정이며 결함 확정이나 수정 완료로 쓰지 않는다. local guardrail의 contextless NAME 오탐 재현은 있으나 production 원인을 입증하지 않는다. 후속에는 비식별 안전 회귀 재현과 production 관찰의 독립 대조가 필요하다. + +## 실배포 RED와 local 수정 증거 + +| ID | 실제 배포 상태 | local 수정·검증 | 배포 재검증 조건 | +|---|---|---|---| +| H09 | `/learn/history`의 검색·필터를 적용한 뒤 특정 기록을 열고 돌아왔을 때 목록 맥락 복귀가 RED다. | `apps/web/src/pages/LearnerHome.tsx`가 history `q`·`status`를 URL search params로 소유하도록 수정했고 `uc-learner-practice-history` local 16 passed(22.9s)다. | 관리자 사용자 경로의 회귀와 순수 learner 계정의 기존 종료 테스트 기록에서 검색·필터→기록/리뷰→브라우저 뒤로 복귀를 실제 배포에서 다시 확인한다. | +| T18 | 감독·연구 화면의 producer drilldown이 legacy 경로를 가리켜 실제 배포에서 RED다. | `apps/api/app/services/supervision_research_producer.py`의 canonical `/teach/session/{id}/review` 경로와 `apps/web/src/pages/SupervisionResearch.tsx`의 legacy 링크 정규화를 수정했다. API 12 passed, local 교수자 15 passed(25.3s)다. | 관리자 사용자 경로의 회귀와 순수 teacher 계정에서 producer가 만든 비식별 signal의 `/teach/supervision`→drilldown→해당 리뷰 경로를 실제 배포에서 다시 확인한다. | + +두 항목의 local 증거는 배포본 교체 또는 실제 배포 재검증 증거가 아니다. 운영 설정·실사용자·승인 원장은 변경하지 않았다. + +## 상태 해석과 재개 조건 + +- `PASS`는 ledger에 실제 CUA 관찰이 연결된 시나리오만 뜻한다. +- `RED`는 실제 배포에서 관찰된 결함이며 local 수정 통과만으로 닫지 않는다. +- `BLOCKED`는 필요한 역할·등록 데이터 또는 명시적 mutation 승인 부족 때문에 실행할 수 없는 상태다. +- `NOT_RUN`은 아직 실행하지 않은 상태이며 실패나 통과를 뜻하지 않는다. +- 실행 재개 전에는 순수 teacher·OFF 계정, 필요한 비식별 등록·종료 회기와 허용된 mutation 범위를 준비하고 ledger의 최신 수치를 다시 읽는다. 독립 관찰자 공개 검증 R10은 여전히 `BLOCKED`다. +- C11은 Tab→Enter로 통과했으며 ArrowDown 동작은 지원하지 않는 한계로 남긴다. S19는 과거 Ctrl+R 증거를 취소했지만, 이번 exact URL 이동 뒤 실제 문서 reload로 재확인해 `PASS`로 확정했다.