feat: 대시보드에 학습 센터 진도 요약 위젯 추가

- 코스 완료율·카테고리별 집계 칩·학습 센터 바로가기
- Rule of Three 학습 포인트 주석 (중복 2회까지는 추상화를 참는다)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
AWESOMEDEV 2026-07-16 15:36:37 +09:00
parent c4c3bb0712
commit f181aef30b

View File

@ -1,9 +1,78 @@
// : (1~8) ,
// . .
// . ,
// .
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import client from '../api/client';
import { useAuth } from '../AuthContext';
import Card from '../components/Card';
import { COURSE_CATEGORIES, ALL_COURSES } from '../courseCatalog';
// .
// : StudentProgressCard " " .
// (
// "Rule of Three").
function CourseProgressWidget() {
const [completedSlugs, setCompletedSlugs] = useState(new Set());
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
client
.get('/course-progress/me')
.then((res) => {
if (!cancelled) setCompletedSlugs(new Set(res.data));
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, []);
const total = ALL_COURSES.length;
const doneCount = ALL_COURSES.filter((c) => completedSlugs.has(c.slug)).length;
const percent = total === 0 ? 0 : Math.round((doneCount / total) * 100);
if (loading) return null; //
return (
<Card style={{ marginBottom: 16 }}>
<div className="assign-title-row" style={{ marginBottom: 10 }}>
<span className="assign-title">📚 학습 센터 진도</span>
<Link to="/learn" className="btn btn-ghost" style={{ marginLeft: 'auto', fontSize: 13 }}>
학습 센터로
</Link>
</div>
<div className="stat-row" style={{ marginBottom: 12 }}>
<div className="stat">
<span className="stat-num" style={{ fontSize: 26 }}>{percent}%</span>
<span className="stat-label">{doneCount} / {total} 코스 완료</span>
</div>
<div className="progress-track" style={{ flex: 1, minWidth: 120 }}>
<div className="progress-fill" style={{ width: `${percent}%` }} />
</div>
</div>
<div className="chip-row" style={{ marginTop: 0 }}>
{COURSE_CATEGORIES.map((group) => {
const groupDone = group.items.filter(
(i) => i.slug && completedSlugs.has(i.slug),
).length;
return (
<span
key={group.category}
className="chip"
style={groupDone > 0 ? { color: 'var(--teal)', fontWeight: 700 } : undefined}
>
{group.category} {groupDone}/{group.items.length}
</span>
);
})}
</div>
</Card>
);
}
const WEEKS = [1, 2, 3, 4, 5, 6, 7, 8];
@ -96,6 +165,9 @@ export default function DashboardPage() {
</div>
</div>
{/* 학습 센터 코스 진도 요약 */}
<CourseProgressWidget />
<div className="tabs">
{WEEKS.map((w) => (
<button