학생 분석 목록 펼침 보강

This commit is contained in:
Yun Chan 2026-07-01 12:41:17 +09:00
parent 51053af536
commit 69fece56bf
9 changed files with 289 additions and 145 deletions

View file

@ -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>

View file

@ -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;
}