From 51b8738c6a93f4bf98c0a3c637895cff7199d92d Mon Sep 17 00:00:00 2001 From: jaemin Date: Tue, 19 May 2026 16:12:05 +0900 Subject: [PATCH] refactor(admin): switch focus-card issuance to vulnerability/weak-actions API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MAT-783. The previous concept-history endpoint only aggregated raw correct/incorrect counts and was inaccurate as a focus-card issuance signal. The vulnerability API exposes server-computed action-level weakness scores (sorted desc) which directly model the issuance domain. - drop apis/controller/analytics - add apis/controller/vulnerability with getStudentWeakActions - remap the issuance page's '취약점' summary to render conceptNodeName, difficulty badge, vulnerability score, problemCount, lastCalculatedAt Co-Authored-By: Claude Opus 4.7 (1M context) --- .../controller/analytics/getConceptHistory.ts | 16 ---- .../src/apis/controller/analytics/index.ts | 3 - .../vulnerability/getStudentWeakActions.ts | 16 ++++ .../apis/controller/vulnerability/index.ts | 3 + apps/admin/src/apis/index.ts | 2 +- .../_GNBLayout/focus-card/issuance/index.tsx | 79 +++++++++++-------- 6 files changed, 64 insertions(+), 55 deletions(-) delete mode 100644 apps/admin/src/apis/controller/analytics/getConceptHistory.ts delete mode 100644 apps/admin/src/apis/controller/analytics/index.ts create mode 100644 apps/admin/src/apis/controller/vulnerability/getStudentWeakActions.ts create mode 100644 apps/admin/src/apis/controller/vulnerability/index.ts diff --git a/apps/admin/src/apis/controller/analytics/getConceptHistory.ts b/apps/admin/src/apis/controller/analytics/getConceptHistory.ts deleted file mode 100644 index ae3cab72..00000000 --- a/apps/admin/src/apis/controller/analytics/getConceptHistory.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { $api } from '@apis'; - -const getConceptHistory = (studentId: number, options?: { enabled?: boolean }) => { - return $api.useQuery( - 'get', - '/api/admin/analytics/concept-history/{studentId}', - { - params: { - path: { studentId }, - }, - }, - options - ); -}; - -export default getConceptHistory; diff --git a/apps/admin/src/apis/controller/analytics/index.ts b/apps/admin/src/apis/controller/analytics/index.ts deleted file mode 100644 index 3cff2fbd..00000000 --- a/apps/admin/src/apis/controller/analytics/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import getConceptHistory from './getConceptHistory'; - -export { getConceptHistory }; diff --git a/apps/admin/src/apis/controller/vulnerability/getStudentWeakActions.ts b/apps/admin/src/apis/controller/vulnerability/getStudentWeakActions.ts new file mode 100644 index 00000000..5a66a807 --- /dev/null +++ b/apps/admin/src/apis/controller/vulnerability/getStudentWeakActions.ts @@ -0,0 +1,16 @@ +import { $api } from '@apis'; + +const getStudentWeakActions = (studentId: number, options?: { enabled?: boolean }) => { + return $api.useQuery( + 'get', + '/api/admin/vulnerability/students/{studentId}/weak-actions', + { + params: { + path: { studentId }, + }, + }, + options + ); +}; + +export default getStudentWeakActions; diff --git a/apps/admin/src/apis/controller/vulnerability/index.ts b/apps/admin/src/apis/controller/vulnerability/index.ts new file mode 100644 index 00000000..636f0b2f --- /dev/null +++ b/apps/admin/src/apis/controller/vulnerability/index.ts @@ -0,0 +1,3 @@ +import getStudentWeakActions from './getStudentWeakActions'; + +export { getStudentWeakActions }; diff --git a/apps/admin/src/apis/index.ts b/apps/admin/src/apis/index.ts index 6b3e47b6..aa115056 100644 --- a/apps/admin/src/apis/index.ts +++ b/apps/admin/src/apis/index.ts @@ -2,7 +2,6 @@ export { $api } from './client'; // controllers -export * from './controller/analytics'; export * from './controller/auth'; export * from './controller/concept'; export * from './controller/conceptGraph'; @@ -24,3 +23,4 @@ export * from './controller/role'; export * from './controller/student'; export * from './controller/teacher'; export * from './controller/user'; +export * from './controller/vulnerability'; diff --git a/apps/admin/src/routes/_GNBLayout/focus-card/issuance/index.tsx b/apps/admin/src/routes/_GNBLayout/focus-card/issuance/index.tsx index e72275a1..c7ec8137 100644 --- a/apps/admin/src/routes/_GNBLayout/focus-card/issuance/index.tsx +++ b/apps/admin/src/routes/_GNBLayout/focus-card/issuance/index.tsx @@ -1,7 +1,7 @@ import { deleteFocusCardIssuance, - getConceptHistory, getFocusCardIssuanceByDate, + getStudentWeakActions, postFocusCardAutoIssue, postFocusCardIssuance, } from '@apis'; @@ -11,7 +11,7 @@ import { InlineProblemViewer } from '@repo/pointer-editor-v2'; import { createFileRoute } from '@tanstack/react-router'; import dayjs from 'dayjs'; import { AlertTriangle, Layers, Plus, Trash2, Wand2 } from 'lucide-react'; -import { useMemo, useState } from 'react'; +import { useState } from 'react'; import { Slide, toast, ToastContainer } from 'react-toastify'; import { NodeSearchSelect } from '@/components/conceptGraph'; @@ -37,18 +37,12 @@ function RouteComponent() { { enabled: isStudentSelected } ); - const { data: conceptHistory, isLoading: isHistoryLoading } = getConceptHistory(studentId, { - enabled: isStudentSelected, - }); + const { data: weakActionsResp, isLoading: isWeakActionsLoading } = getStudentWeakActions( + studentId, + { enabled: isStudentSelected } + ); - const vulnerableConcepts = useMemo(() => { - const stats = conceptHistory?.conceptStats ?? []; - // 시도 횟수 5회 이상 + 정답률 낮은 순. 시도 횟수가 너무 적으면 노이즈라 제외. - return [...stats] - .filter((s) => (s.totalAttempts ?? 0) >= 5) - .sort((a, b) => (a.correctRate ?? 100) - (b.correctRate ?? 100)) - .slice(0, 5); - }, [conceptHistory]); + const weakActions = (weakActionsResp?.data ?? []).slice(0, 5); const { mutate: mutateIssue, isPending: isIssuing } = postFocusCardIssuance(); const { mutate: mutateRevoke } = deleteFocusCardIssuance(); @@ -188,31 +182,44 @@ function RouteComponent() {
-

학생 취약점

- - (시도 5회 이상 · 정답률 낮은 순 상위 5개) - +

학생 약점 액션

+ (취약도 높은 순 상위 5개) - {isHistoryLoading ? ( -

취약점 분석 중...

- ) : vulnerableConcepts.length === 0 ? ( + {isWeakActionsLoading ? ( +

약점 분석 중...

+ ) : weakActions.length === 0 ? (

- 분석할 만큼 풀이 데이터가 충분하지 않습니다. (개념별 5회 이상 시도 필요) + 산정된 약점 액션이 없습니다.

) : (
- {vulnerableConcepts.map((stat) => { - const rate = stat.correctRate ?? 0; - const isCritical = rate < 40; - const isWarning = rate >= 40 && rate < 60; + {weakActions.map((action) => { + const vulnerability = action.vulnerability ?? 0; + const isCritical = vulnerability >= 0.7; + const isWarning = vulnerability >= 0.4 && vulnerability < 0.7; + const difficulty = action.difficultyLevel; + const difficultyBadgeClass = + difficulty === 'HIGH' + ? 'bg-red-100 text-red-700' + : difficulty === 'MEDIUM' + ? 'bg-amber-100 text-amber-700' + : 'bg-gray-100 text-gray-600'; return (
-

- {stat.conceptName ?? '이름 없음'} -

+
+

+ {action.conceptNodeName ?? '이름 없음'} +

+ {difficulty && ( + + {difficulty} + + )} +
- {Math.round(rate)}% + {vulnerability.toFixed(2)} - 정답률 + 취약도

- {stat.correctCount ?? 0} / {stat.totalAttempts ?? 0}회 정답 + 문제 {action.problemCount ?? 0}개 ·{' '} + {action.lastCalculatedAt + ? dayjs(action.lastCalculatedAt).format('MM/DD HH:mm') + : '-'}

); @@ -235,10 +245,9 @@ function RouteComponent() {
)} - {vulnerableConcepts.length > 0 && ( + {weakActions.length > 0 && (

- ※ 개념(Concept)과 카드의 Action Node는 다른 축이므로, 위 취약 개념을 참고해 직접 - '카드 발급'으로 적절한 Action Node를 골라 발급하세요. + ※ 위 약점 액션을 참고해 '카드 발급'으로 적절한 Action Node를 골라 발급하세요.

)}