feat: 대시보드에 학습 센터 진도 요약 위젯 추가
- 코스 완료율·카테고리별 집계 칩·학습 센터 바로가기 - Rule of Three 학습 포인트 주석 (중복 2회까지는 추상화를 참는다) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c4c3bb0712
commit
f181aef30b
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user