Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 0 additions & 16 deletions apps/admin/src/apis/controller/analytics/getConceptHistory.ts

This file was deleted.

3 changes: 0 additions & 3 deletions apps/admin/src/apis/controller/analytics/index.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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;
3 changes: 3 additions & 0 deletions apps/admin/src/apis/controller/vulnerability/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import getStudentWeakActions from './getStudentWeakActions';

export { getStudentWeakActions };
2 changes: 1 addition & 1 deletion apps/admin/src/apis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -24,3 +23,4 @@ export * from './controller/role';
export * from './controller/student';
export * from './controller/teacher';
export * from './controller/user';
export * from './controller/vulnerability';
79 changes: 44 additions & 35 deletions apps/admin/src/routes/_GNBLayout/focus-card/issuance/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
deleteFocusCardIssuance,
getConceptHistory,
getFocusCardIssuanceByDate,
getStudentWeakActions,
postFocusCardAutoIssue,
postFocusCardIssuance,
} from '@apis';
Expand All @@ -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';
Expand All @@ -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();
Expand Down Expand Up @@ -188,31 +182,44 @@ function RouteComponent() {
<div className='flex h-8 w-8 items-center justify-center rounded-xl bg-amber-100 text-amber-700'>
<AlertTriangle className='h-4 w-4' />
</div>
<h3 className='text-base font-bold text-gray-900'>학생 취약점</h3>
<span className='text-xs text-gray-500'>
(시도 5회 이상 · 정답률 낮은 순 상위 5개)
</span>
<h3 className='text-base font-bold text-gray-900'>학생 약점 액션</h3>
<span className='text-xs text-gray-500'>(취약도 높은 순 상위 5개)</span>
</div>

{isHistoryLoading ? (
<p className='py-4 text-center text-sm text-gray-400'>취약점 분석 중...</p>
) : vulnerableConcepts.length === 0 ? (
{isWeakActionsLoading ? (
<p className='py-4 text-center text-sm text-gray-400'>약점 분석 중...</p>
) : weakActions.length === 0 ? (
<p className='py-4 text-center text-sm text-gray-500'>
분석할 만큼 풀이 데이터가 충분하지 않습니다. (개념별 5회 이상 시도 필요)
산정된 약점 액션이 없습니다.
</p>
) : (
<div className='grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-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 (
<div
key={stat.conceptId}
key={action.conceptNodeId}
className='space-y-1 rounded-xl border border-gray-200 bg-white p-3'>
<p className='line-clamp-2 text-xs font-semibold text-gray-800'>
{stat.conceptName ?? '이름 없음'}
</p>
<div className='flex items-start justify-between gap-1'>
<p className='line-clamp-2 text-xs font-semibold text-gray-800'>
{action.conceptNodeName ?? '이름 없음'}
</p>
{difficulty && (
<span
className={`shrink-0 rounded-md px-1.5 py-0.5 text-[10px] font-semibold ${difficultyBadgeClass}`}>
{difficulty}
</span>
)}
</div>
<div className='flex items-baseline gap-1'>
<span
className={`text-lg font-bold ${
Expand All @@ -222,23 +229,25 @@ function RouteComponent() {
? 'text-amber-600'
: 'text-gray-700'
}`}>
{Math.round(rate)}%
{vulnerability.toFixed(2)}
</span>
<span className='text-[10px] text-gray-400'>정답률</span>
<span className='text-[10px] text-gray-400'>취약도</span>
</div>
<p className='text-[10px] text-gray-500'>
{stat.correctCount ?? 0} / {stat.totalAttempts ?? 0}회 정답
문제 {action.problemCount ?? 0}개 ·{' '}
{action.lastCalculatedAt
? dayjs(action.lastCalculatedAt).format('MM/DD HH:mm')
: '-'}
</p>
</div>
);
})}
</div>
)}

{vulnerableConcepts.length > 0 && (
{weakActions.length > 0 && (
<p className='pt-1 text-[11px] text-gray-500'>
※ 개념(Concept)과 카드의 Action Node는 다른 축이므로, 위 취약 개념을 참고해 직접
'카드 발급'으로 적절한 Action Node를 골라 발급하세요.
※ 위 약점 액션을 참고해 '카드 발급'으로 적절한 Action Node를 골라 발급하세요.
</p>
)}
</section>
Expand Down
Loading