학생 분석 목록 펼침 보강
This commit is contained in:
parent
51053af536
commit
69fece56bf
9 changed files with 289 additions and 145 deletions
|
|
@ -833,6 +833,7 @@ function LearnerOverviewTable({
|
|||
onSort: (value: LearnerSort) => void;
|
||||
onOpen: (learnerId: string) => void;
|
||||
}) {
|
||||
const [expandedLearnerId, setExpandedLearnerId] = useState<string | null>(null);
|
||||
const sortOptions: Array<{ key: LearnerSort; label: string }> = [
|
||||
{ key: "latest", label: "최근순" },
|
||||
{ key: "sessions", label: "회기순" },
|
||||
|
|
@ -840,6 +841,12 @@ function LearnerOverviewTable({
|
|||
{ key: "delta", label: "변화순" },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (expandedLearnerId && !learners.some((learner) => learner.learner_id === expandedLearnerId)) {
|
||||
setExpandedLearnerId(null);
|
||||
}
|
||||
}, [expandedLearnerId, learners]);
|
||||
|
||||
return (
|
||||
<section className="pf-section pf-section--learner-overview">
|
||||
<div className="pf-section__head">
|
||||
|
|
@ -890,67 +897,128 @@ function LearnerOverviewTable({
|
|||
<div className="pf-learner-table__head" aria-hidden="true">
|
||||
<span>학습자</span>
|
||||
<span>회기</span>
|
||||
<span>추이</span>
|
||||
<span>최근 적절성</span>
|
||||
<span>변화</span>
|
||||
<span>평균 라포</span>
|
||||
<span>기법</span>
|
||||
<span>추이</span>
|
||||
<span>기법 요약</span>
|
||||
<span>펼침</span>
|
||||
<span>상세</span>
|
||||
</div>
|
||||
{learners.map((learner) => (
|
||||
<article
|
||||
className="pf-learner-row"
|
||||
data-learner-overview-row="true"
|
||||
key={learner.learner_id}
|
||||
>
|
||||
<div className="pf-learner-row__id">
|
||||
<b>{learner.learner_label}</b>
|
||||
<span>{learner.learner_id}</span>
|
||||
</div>
|
||||
<div className="pf-learner-row__metric" data-label="회기">
|
||||
<b>
|
||||
{learner.ended_sessions}/{learner.sessions}
|
||||
</b>
|
||||
<span>{formatDateTime(learner.latest_at)}</span>
|
||||
</div>
|
||||
<div className="pf-learner-row__spark" data-label="추이">
|
||||
<div className="pf-learner-spark" aria-label={`${learner.learner_label} 회기별 추이`}>
|
||||
{(learner.points ?? []).map((point) => (
|
||||
<GrowthBar point={point} key={point.session_id} />
|
||||
))}
|
||||
</div>
|
||||
<Badge tone={learner.trend === "down" ? "warn" : learner.trend === "up" ? "accent" : "neutral"}>
|
||||
{trendLabel(learner.trend)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="pf-learner-row__metric" data-label="최근 적절성">
|
||||
<b>{formatScore(learner.latest_score)}</b>
|
||||
</div>
|
||||
<div className="pf-learner-row__metric" data-label="변화">
|
||||
<b>{formatDelta(learner.score_delta)}</b>
|
||||
</div>
|
||||
<div className="pf-learner-row__metric" data-label="평균 라포">
|
||||
<b>{formatRapport(learner.avg_rapport)}</b>
|
||||
</div>
|
||||
<div className="pf-learner-row__tags" data-label="기법">
|
||||
{(learner.top_techniques ?? []).slice(0, 3).map((tag) => (
|
||||
<span key={tag}>{tag}</span>
|
||||
))}
|
||||
{(learner.top_techniques ?? []).length === 0 ? <span>기법 부족</span> : null}
|
||||
</div>
|
||||
<div className="pf-learner-row__action">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
leading={<Icon name="chevron-right" size={14} />}
|
||||
aria-label={`${learner.learner_label} 상세 분석 열기`}
|
||||
onClick={() => onOpen(learner.learner_id)}
|
||||
{learners.map((learner) => {
|
||||
const isExpanded = expandedLearnerId === learner.learner_id;
|
||||
const expandId = `learner-expanded-${learner.learner_id}`;
|
||||
const topTechniques = learner.top_techniques ?? [];
|
||||
return (
|
||||
<div className={`pf-learner-record ${isExpanded ? "is-expanded" : ""}`} key={learner.learner_id}>
|
||||
<article
|
||||
className="pf-learner-row"
|
||||
data-learner-overview-row="true"
|
||||
>
|
||||
상세 분석
|
||||
</Button>
|
||||
<div className="pf-learner-row__id">
|
||||
<button
|
||||
type="button"
|
||||
className="pf-learner-name-button"
|
||||
onClick={() => onOpen(learner.learner_id)}
|
||||
>
|
||||
{learner.learner_label}
|
||||
</button>
|
||||
<span>{learner.learner_id}</span>
|
||||
</div>
|
||||
<div className="pf-learner-row__metric" data-label="회기">
|
||||
<b>
|
||||
{learner.ended_sessions}/{learner.sessions}
|
||||
</b>
|
||||
<span>{formatDateTime(learner.latest_at)}</span>
|
||||
</div>
|
||||
<div className="pf-learner-row__metric" data-label="최근 적절성">
|
||||
<b>{formatScore(learner.latest_score)}</b>
|
||||
</div>
|
||||
<div className="pf-learner-row__metric" data-label="변화">
|
||||
<b>{formatDelta(learner.score_delta)}</b>
|
||||
</div>
|
||||
<div className="pf-learner-row__metric" data-label="평균 라포">
|
||||
<b>{formatRapport(learner.avg_rapport)}</b>
|
||||
</div>
|
||||
<div className="pf-learner-row__trend" data-label="추이">
|
||||
<Badge tone={learner.trend === "down" ? "warn" : learner.trend === "up" ? "accent" : "neutral"}>
|
||||
{trendLabel(learner.trend)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="pf-learner-row__tag-summary" data-label="기법 요약">
|
||||
<b>{topTechniques[0] ?? "기법 부족"}</b>
|
||||
{topTechniques.length > 1 ? <span>외 {topTechniques.length - 1}개</span> : null}
|
||||
</div>
|
||||
<div className="pf-learner-row__expand">
|
||||
<button
|
||||
type="button"
|
||||
className="pf-icon-action"
|
||||
aria-expanded={isExpanded}
|
||||
aria-controls={expandId}
|
||||
aria-label={`${learner.learner_label} 요약 ${isExpanded ? "접기" : "펼치기"}`}
|
||||
onClick={() => setExpandedLearnerId(isExpanded ? null : learner.learner_id)}
|
||||
>
|
||||
<Icon name={isExpanded ? "arrow-up" : "arrow-down"} size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="pf-learner-row__action">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
leading={<Icon name="chevron-right" size={14} />}
|
||||
aria-label={`${learner.learner_label} 상세 보기`}
|
||||
onClick={() => onOpen(learner.learner_id)}
|
||||
>
|
||||
상세 보기
|
||||
</Button>
|
||||
</div>
|
||||
</article>
|
||||
{isExpanded ? (
|
||||
<div
|
||||
className="pf-learner-row-detail"
|
||||
id={expandId}
|
||||
data-learner-expanded-row="true"
|
||||
>
|
||||
<div className="pf-learner-detail-item pf-learner-detail-item--trend">
|
||||
<span>회기별 추이</span>
|
||||
{(learner.points ?? []).length > 0 ? (
|
||||
<div className="pf-learner-spark" aria-label={`${learner.learner_label} 회기별 추이`}>
|
||||
{(learner.points ?? []).map((point) => (
|
||||
<GrowthBar point={point} key={point.session_id} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<small>저장된 점수 없음</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="pf-learner-detail-item">
|
||||
<span>점수 변화</span>
|
||||
<b>
|
||||
{formatScore(learner.first_score)} → {formatScore(learner.latest_score)}
|
||||
</b>
|
||||
<small>{formatDelta(learner.score_delta)}</small>
|
||||
</div>
|
||||
<div className="pf-learner-detail-item">
|
||||
<span>최근 기록</span>
|
||||
<b>{formatDateTime(learner.latest_at)}</b>
|
||||
<small>
|
||||
완료 {learner.ended_sessions} / 전체 {learner.sessions}
|
||||
</small>
|
||||
</div>
|
||||
<div className="pf-learner-detail-item pf-learner-detail-item--tags">
|
||||
<span>기법 태그</span>
|
||||
<div className="pf-learner-row__tags">
|
||||
{topTechniques.slice(0, 5).map((tag) => (
|
||||
<span key={tag}>{tag}</span>
|
||||
))}
|
||||
{topTechniques.length === 0 ? <span>기법 부족</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -405,22 +405,25 @@
|
|||
min-width:0;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
overflow-x:auto;
|
||||
scrollbar-gutter:stable;
|
||||
}
|
||||
.pf-learner-table__head,
|
||||
.pf-learner-row{
|
||||
display:grid;
|
||||
grid-template-columns:
|
||||
minmax(170px,1.25fr)
|
||||
minmax(178px,1.3fr)
|
||||
minmax(86px,.55fr)
|
||||
minmax(164px,.9fr)
|
||||
minmax(76px,.48fr)
|
||||
minmax(64px,.42fr)
|
||||
minmax(76px,.48fr)
|
||||
minmax(118px,.75fr)
|
||||
minmax(96px,.42fr);
|
||||
minmax(70px,.42fr)
|
||||
minmax(112px,.72fr)
|
||||
48px
|
||||
minmax(104px,.5fr);
|
||||
gap:9px;
|
||||
align-items:center;
|
||||
min-width:0;
|
||||
min-width:1000px;
|
||||
}
|
||||
.pf-learner-table__head{
|
||||
position:sticky;
|
||||
|
|
@ -434,19 +437,24 @@
|
|||
font-weight:800;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-learner-row{
|
||||
padding:11px 12px;
|
||||
.pf-learner-record{
|
||||
min-width:1000px;
|
||||
border-top:1px solid var(--paper-2);
|
||||
}
|
||||
.pf-learner-table__head + .pf-learner-row{
|
||||
.pf-learner-table__head + .pf-learner-record{
|
||||
border-top:0;
|
||||
}
|
||||
.pf-learner-row:hover{
|
||||
.pf-learner-row{
|
||||
padding:10px 12px;
|
||||
}
|
||||
.pf-learner-record:hover .pf-learner-row,
|
||||
.pf-learner-record.is-expanded .pf-learner-row{
|
||||
background:color-mix(in srgb,var(--accent-tint) 14%,transparent);
|
||||
}
|
||||
.pf-learner-row__id,
|
||||
.pf-learner-row__metric,
|
||||
.pf-learner-row__spark,
|
||||
.pf-learner-row__trend,
|
||||
.pf-learner-row__tag-summary,
|
||||
.pf-learner-row__tags{
|
||||
min-width:0;
|
||||
}
|
||||
|
|
@ -454,13 +462,36 @@
|
|||
display:grid;
|
||||
gap:3px;
|
||||
}
|
||||
.pf-learner-row__id b{
|
||||
.pf-learner-name-button{
|
||||
width:max-content;
|
||||
max-width:100%;
|
||||
padding:0;
|
||||
border:0;
|
||||
color:var(--text-strong);
|
||||
background:transparent;
|
||||
font:inherit;
|
||||
font-size:var(--fs-sm);
|
||||
font-weight:820;
|
||||
line-height:1.35;
|
||||
text-align:left;
|
||||
cursor:pointer;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-learner-name-button:hover,
|
||||
.pf-learner-name-button:focus-visible{
|
||||
color:var(--accent-deep);
|
||||
text-decoration:underline;
|
||||
text-underline-offset:3px;
|
||||
}
|
||||
.pf-learner-name-button:focus-visible{
|
||||
outline:2px solid var(--accent);
|
||||
outline-offset:2px;
|
||||
}
|
||||
.pf-learner-row__id span,
|
||||
.pf-learner-row__metric span{
|
||||
.pf-learner-row__metric span,
|
||||
.pf-learner-row__tag-summary span{
|
||||
color:var(--text-muted);
|
||||
font-size:11px;
|
||||
line-height:1.3;
|
||||
|
|
@ -473,7 +504,8 @@
|
|||
gap:3px;
|
||||
}
|
||||
.pf-learner-row__metric::before,
|
||||
.pf-learner-row__spark::before,
|
||||
.pf-learner-row__trend::before,
|
||||
.pf-learner-row__tag-summary::before,
|
||||
.pf-learner-row__tags::before{
|
||||
display:none;
|
||||
content:attr(data-label);
|
||||
|
|
@ -488,11 +520,24 @@
|
|||
line-height:1.15;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-learner-row__spark{
|
||||
display:grid;
|
||||
grid-template-columns:minmax(0,1fr) auto;
|
||||
gap:8px;
|
||||
.pf-learner-row__trend{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
min-height:30px;
|
||||
}
|
||||
.pf-learner-row__tag-summary{
|
||||
display:grid;
|
||||
gap:3px;
|
||||
}
|
||||
.pf-learner-row__tag-summary b{
|
||||
min-width:0;
|
||||
color:var(--text-body);
|
||||
font-size:var(--fs-xs);
|
||||
font-weight:760;
|
||||
line-height:1.25;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-learner-spark{
|
||||
min-width:0;
|
||||
|
|
@ -529,14 +574,88 @@
|
|||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-learner-row__expand{
|
||||
display:flex;
|
||||
justify-content:center;
|
||||
}
|
||||
.pf-icon-action{
|
||||
width:34px;
|
||||
height:34px;
|
||||
display:inline-grid;
|
||||
place-items:center;
|
||||
border:1px solid var(--paper-2);
|
||||
border-radius:var(--radius-sm);
|
||||
color:var(--text-muted);
|
||||
background:var(--bg-surface-2);
|
||||
cursor:pointer;
|
||||
}
|
||||
.pf-icon-action:hover,
|
||||
.pf-icon-action:focus-visible,
|
||||
.pf-icon-action[aria-expanded="true"]{
|
||||
color:var(--accent-deep);
|
||||
border-color:var(--accent);
|
||||
background:var(--accent-tint);
|
||||
}
|
||||
.pf-icon-action:focus-visible{
|
||||
outline:2px solid var(--accent);
|
||||
outline-offset:2px;
|
||||
}
|
||||
.pf-learner-row__action{
|
||||
display:flex;
|
||||
justify-content:flex-end;
|
||||
}
|
||||
.pf-learner-row__action .vg-btn{
|
||||
min-width:92px;
|
||||
min-width:96px;
|
||||
padding-inline:10px;
|
||||
}
|
||||
.pf-learner-row-detail{
|
||||
min-width:1000px;
|
||||
display:grid;
|
||||
grid-template-columns:minmax(260px,1.4fr) minmax(140px,.72fr) minmax(170px,.85fr) minmax(190px,1fr);
|
||||
gap:0;
|
||||
padding:0 12px 12px;
|
||||
background:color-mix(in srgb,var(--accent-tint) 12%,transparent);
|
||||
}
|
||||
.pf-learner-detail-item{
|
||||
min-width:0;
|
||||
display:grid;
|
||||
align-content:start;
|
||||
gap:5px;
|
||||
padding:11px 12px;
|
||||
border-top:1px solid var(--hair);
|
||||
border-left:1px solid var(--hair);
|
||||
background:color-mix(in srgb,var(--bg-surface) 90%,var(--bg-surface-2));
|
||||
}
|
||||
.pf-learner-detail-item:first-child{
|
||||
border-left:0;
|
||||
}
|
||||
.pf-learner-detail-item > span{
|
||||
color:var(--text-muted);
|
||||
font-size:var(--fs-xs);
|
||||
font-weight:800;
|
||||
line-height:1.25;
|
||||
}
|
||||
.pf-learner-detail-item > b{
|
||||
min-width:0;
|
||||
color:var(--text-strong);
|
||||
font-family:var(--font-num);
|
||||
font-size:14px;
|
||||
line-height:1.2;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.pf-learner-detail-item > small{
|
||||
color:var(--text-muted);
|
||||
font-size:11px;
|
||||
line-height:1.3;
|
||||
}
|
||||
.pf-learner-detail-item--trend{
|
||||
gap:8px;
|
||||
}
|
||||
.pf-learner-detail-item--tags .pf-learner-row__tags{
|
||||
align-self:center;
|
||||
}
|
||||
.pf-recent__review-state{
|
||||
color:var(--text-muted);
|
||||
font-size:12px;
|
||||
|
|
@ -1247,47 +1366,6 @@
|
|||
.pf-learner-table{
|
||||
background:var(--bg-surface-2);
|
||||
}
|
||||
.pf-learner-table__head{
|
||||
display:none;
|
||||
}
|
||||
.pf-learner-row{
|
||||
margin:10px;
|
||||
padding:12px;
|
||||
border:1px solid var(--paper-2);
|
||||
border-radius:var(--radius);
|
||||
background:var(--bg-surface);
|
||||
grid-template-columns:repeat(3,minmax(0,1fr));
|
||||
gap:10px 12px;
|
||||
}
|
||||
.pf-learner-row + .pf-learner-row{
|
||||
margin-top:0;
|
||||
}
|
||||
.pf-learner-row__id,
|
||||
.pf-learner-row__spark,
|
||||
.pf-learner-row__tags,
|
||||
.pf-learner-row__action{
|
||||
grid-column:1 / -1;
|
||||
}
|
||||
.pf-learner-row__metric::before,
|
||||
.pf-learner-row__spark::before,
|
||||
.pf-learner-row__tags::before{
|
||||
display:block;
|
||||
}
|
||||
.pf-learner-row__spark,
|
||||
.pf-learner-row__tags{
|
||||
display:grid;
|
||||
grid-template-columns:minmax(76px,.28fr) minmax(0,1fr);
|
||||
align-items:center;
|
||||
}
|
||||
.pf-learner-row__tags{
|
||||
display:flex;
|
||||
}
|
||||
.pf-learner-row__tags::before{
|
||||
flex:0 0 76px;
|
||||
}
|
||||
.pf-learner-row__action{
|
||||
justify-content:flex-start;
|
||||
}
|
||||
.pf-analysis__top{
|
||||
grid-template-columns:1fr;
|
||||
}
|
||||
|
|
@ -1507,21 +1585,6 @@
|
|||
.pf-sort-segment button:nth-child(n+3){
|
||||
border-top:1px solid var(--hair);
|
||||
}
|
||||
.pf-learner-row{
|
||||
grid-template-columns:repeat(2,minmax(0,1fr));
|
||||
margin:8px;
|
||||
}
|
||||
.pf-learner-row__spark,
|
||||
.pf-learner-row__tags{
|
||||
grid-template-columns:1fr;
|
||||
gap:6px;
|
||||
}
|
||||
.pf-learner-row__tags::before{
|
||||
flex-basis:100%;
|
||||
}
|
||||
.pf-learner-row__action .vg-btn{
|
||||
width:100%;
|
||||
}
|
||||
.pf-analysis__metrics{
|
||||
grid-template-columns:1fr;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue