회기 집계와 기록 복귀 및 감독 근거 링크 수정
Some checks failed
API contract / OpenAPI type drift (push) Failing after 4m46s

This commit is contained in:
Yun Chan 2026-09-09 14:41:54 +09:00
parent aa951c6c4a
commit 1306c524c1
20 changed files with 4631 additions and 60 deletions

View file

@ -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<MemoryLoadState>("idle");
const [historyFilter, setHistoryFilter] = useState<HistoryFilter>("all");
const [historyQuery, setHistoryQuery] = useState("");
const [localHistoryFilter, setLocalHistoryFilter] =
useState<HistoryFilter>("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<string | null>(null);
const [archiveNotice, setArchiveNotice] = useState<string | null>(null);
// 중간 폭(≤1180px)에서는 대시보드 섹션을 상단 가로 탭으로 전환한다(2026-07-15 소유자 피드백).
@ -177,6 +191,46 @@ export default function LearnerHome({ view = "dashboard" }: LearnerHomeProps) {
const [dashTab, setDashTab] = useState<DashboardTab>("today");
const dashTabRefs = useRef<Array<HTMLButtonElement | null>>([]);
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<HTMLButtonElement>,
currentIndex: number,

View file

@ -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 (
<div className="g6-workbench">