학생 분석 목록 펼침 보강
This commit is contained in:
parent
51053af536
commit
69fece56bf
9 changed files with 289 additions and 145 deletions
|
|
@ -577,6 +577,11 @@ test.describe("layout visual gate @single-run", () => {
|
|||
await expect(page.locator('[data-learner-overview-row="true"]')).toHaveCount(1);
|
||||
await expect(page.locator('[data-learner-analysis-panel="true"]')).toHaveCount(0);
|
||||
await expect(page.getByLabel("학습자 검색")).toBeVisible();
|
||||
const expandButton = page.getByRole("button", { name: /분석 검증 학습자 요약/ });
|
||||
if ((await expandButton.getAttribute("aria-expanded")) !== "true") {
|
||||
await expandButton.click();
|
||||
}
|
||||
await expect(page.locator('[data-learner-expanded-row="true"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -651,7 +651,11 @@ test.describe("teacher console", () => {
|
|||
.filter({ hasText: "사용자 B" });
|
||||
await expect(learnerBRow).toHaveCount(1);
|
||||
await expect(learnerBRow.getByText(learnerB)).toBeVisible();
|
||||
await learnerBRow.getByRole("button", { name: "사용자 B 상세 분석 열기" }).click();
|
||||
await expect(learnerBRow.getByRole("button", { name: "사용자 B", exact: true })).toBeVisible();
|
||||
await learnerBRow.getByRole("button", { name: "사용자 B 요약 펼치기" }).click();
|
||||
await expect(page.locator('[data-learner-expanded-row="true"]')).toBeVisible();
|
||||
await expect(page.locator('[data-learner-expanded-row="true"]').getByText("회기별 추이")).toBeVisible();
|
||||
await learnerBRow.getByRole("button", { name: "사용자 B 상세 보기" }).click();
|
||||
await expect(page).toHaveURL(new RegExp(`/teach/analysis\\?learner=${learnerB}$`));
|
||||
|
||||
const analysisPanel = page.locator('[data-learner-analysis-panel="true"]');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
- 공개 웹: `https://vignette.chanpaca.net`
|
||||
- 공개 API: `https://api-vignette.chanpaca.net`
|
||||
- 최신 앱 배포 소스: 2026-07-01 `origin/master` manual deploy snapshot.
|
||||
- 최신 Cloudflare Pages production deploy: 2026-07-01 manual deploy, branch `main`, custom domain assets `assets/index-DgN8JVBw.js` + `assets/index--s3xqgO5.css`.
|
||||
- 최신 Cloudflare Pages production deploy: 2026-07-01 manual deploy, branch `main`, custom domain assets `assets/index-Cz4PBoO3.js` + `assets/index-DtVTvS9j.css`.
|
||||
- Google OAuth 허용 이메일 도메인: `hs.ac.kr`, `twentyoz.kr`
|
||||
- 최신 백엔드 회귀: `C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -m pytest app/ -q` → `305 passed`
|
||||
- X1 재귀학습 export 1차: `scripts/export-recursive-dataset.py` 기본 read-only dry-run, `--write-dataset` 명시 시에만 `ds.*` write, approved export는 steward/legal/IAA gate 없으면 거부.
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
- `frontenddesign` 스킬은 현재 세션의 사용 가능 스킬 목록에 없었다. 대신 `docs/DESIGN_CONCEPT.md`를 SSOT로 사용했다.
|
||||
- 공개 API 터널은 현재 `C:\Users\encep\.cloudflared\vignette-config.yml`에서 `http://127.0.0.1:8001`을 본다.
|
||||
- 공개용 API 프로세스는 `127.0.0.1:8001`에서 `ENVIRONMENT=prod`로 떠 있다. 로컬 개발 API `127.0.0.1:8000`과 Tailnet 개발 API `127.0.0.1:8010`도 `ENVIRONMENT=dev`로 떠 있다.
|
||||
- 2026-06-27 05:34:32 UTC 모바일 502는 cloudflared 로그상 `127.0.0.1:8001` origin connection refused와 일치한다. 현재 public API는 복구됐고, watchdog 기본 검사에서 아직 DNS가 없는 `api-vnet.18ka.net`을 제외해 향후 설치 시 불필요한 restart loop를 막았다. 2026-07-01 KST 재확인 대상: `https://vignette.chanpaca.net/login?deploy=20260701` 200 with `assets/index-DgN8JVBw.js` + `assets/index--s3xqgO5.css`, `https://api-vignette.chanpaca.net/health` prod/db/engine true, public unauth `GET /personas` 401.
|
||||
- 2026-06-27 05:34:32 UTC 모바일 502는 cloudflared 로그상 `127.0.0.1:8001` origin connection refused와 일치한다. 현재 public API는 복구됐고, watchdog 기본 검사에서 아직 DNS가 없는 `api-vnet.18ka.net`을 제외해 향후 설치 시 불필요한 restart loop를 막았다. 2026-07-01 KST 재확인 대상: `https://vignette.chanpaca.net/login?deploy=20260701` 200 with `assets/index-Cz4PBoO3.js` + `assets/index-DtVTvS9j.css`, `https://api-vignette.chanpaca.net/health` prod/db/engine true, public unauth `GET /personas` 401.
|
||||
- `GET /personas`는 이제 인증 필요다. 검증 당시 로컬 `http://127.0.0.1:8000/personas`와 현재 공개 `https://api-vignette.chanpaca.net/personas` 모두 비로그인 401 확인 완료.
|
||||
- 로컬 웹이 `http://127.0.0.1:5175`처럼 다른 Vite 포트로 떠도 로그인은 `로컬 테스트 계정으로 계속` 버튼을 사용한다. Google OAuth 버튼은 로컬에서는 disabled로 둔다. 현재 OAuth callback이 공개 API로 돌아가기 때문에 로컬 Google OAuth는 로컬 세션에 붙을 수 없다.
|
||||
- 현재 확인용 프로세스는 `127.0.0.1:9099` engine gateway, `127.0.0.1:8001` prod public API, `127.0.0.1:8000` dev API, `127.0.0.1:8010` Tailnet dev API, `127.0.0.1:5173` Vite dev web, `127.0.0.1:5174` public preview, `cloudflared` tunnel 1개다. Tailnet URL은 `https://alpaca-home.taile93291.ts.net/login`이다. stale Vite allowedHosts로 403이 났던 상태는 재검증 시 `login` 200, `/api/health` dev/db/engine true, `/api/auth/config` 200이었다. `alpaca-home.taile93291.ts.net`는 Vite 기본 `allowedHosts`에도 포함했다.
|
||||
|
|
@ -170,7 +170,7 @@
|
|||
- Public web deployment:
|
||||
- `wrangler pages deploy dist --project-name vignette --branch main --commit-dirty=true`
|
||||
- 2026-06-29 manual deployment, preview `https://eb2ed257.vignette-b1q.pages.dev`
|
||||
- `https://vignette.chanpaca.net/` serves `assets/index-DgN8JVBw.js` and `assets/index--s3xqgO5.css`
|
||||
- `https://vignette.chanpaca.net/` serves `assets/index-Cz4PBoO3.js` and `assets/index-DtVTvS9j.css`
|
||||
- `https://vignette.chanpaca.net/live2d/mao/Mao.model3.json`, Haru model, and Cubism core routes return 404
|
||||
- Public login screen:
|
||||
- `npx playwright test e2e/auth.spec.ts --project=chromium-desktop --grep "public login"`
|
||||
|
|
|
|||
|
|
@ -518,7 +518,7 @@
|
|||
<p class="dg-note">82차 적용(2026-06-29): refactor-governance P8로 <code>Session.tsx</code>의 음성/세션 UI 파생 상태 계산을 <code>sessionVoiceStatusView()</code> helper로 분리했다. 마이크 busy/label, 세션 상태 label, transcript live label, 텍스트 입력 차단, 응답/음성 상태 chip 계산을 named view-model 경계에서 유지한다. API/DB 계약, 세션 시작·전송·종료, voice WebSocket/TTS lifecycle, live-coach payload, DOM/CSS는 변경하지 않았다. 검증: <code>npm run typecheck</code>, <code>npm run build</code>, <code>session-mvp</code> 1 passed, <code>voice-success</code> 2 passed, <code>session-layout</code> desktop/mobile 8 passed.</p>
|
||||
<p class="dg-note">83차 적용(2026-06-29): refactor-governance P3로 학습자/교수자 성장 지표가 턴 평가 기법 라벨을 읽는 경계를 <code>session_metrics.turn_technique_label()</code>로 명시했다. DB 정규화 후 재수화되는 <code>TechniqueTag.label_ko</code>를 우선 사용하고, 기존 <code>label/name/id/code</code> legacy fallback은 유지한다. <code>alternative_utterances</code>는 future adapter 추출 전에 문자열 배열과 <code>text/suggestion</code> dict 배열을 모두 row로 정규화하는 호환 테스트로 고정했다. 검증: py_compile, evaluation/learner/teacher focused 59 passed.</p>
|
||||
<p class="dg-note">84차 적용(2026-06-29): refactor-governance P2로 <code>Session.tsx</code>의 음성 guard/view-model 경계를 한 번 더 줄였다. <code>isVoiceStatusBusy()</code>가 마이크 busy 판정을 소유하고, <code>sessionVoiceStatusView()</code>가 마이크 disabled와 aria label까지 반환해 JSX와 <code>toggleMic()</code>의 중복 조건을 제거했다. <code>recording</code>은 계속 busy가 아니므로 두 번째 마이크 클릭으로 발화 전송이 가능하다. API/DB 계약, WebSocket/TTS lifecycle, STT/TTS provider 동작, live-coach payload, DOM/CSS는 변경하지 않았다. 검증: <code>npm run typecheck</code>, <code>npm run build</code>, <code>session-mvp</code> 1 passed, <code>voice-success</code> 2 passed, <code>session-layout</code> desktop/mobile 8 passed.</p>
|
||||
<p class="dg-note">85차 적용(2026-06-29): refactor-governance P3로 Node 전환 계약 검증의 <code>gateway-default</code> sentinel 중복을 줄였다. <code>scripts/check-engine-gateway-contract.mjs</code>는 더 이상 sentinel 문자열을 자체 상수로 들고 있지 않고, <code>engine_gateway_schema.v1.json</code>의 <code>x-engine-gateway-default-model-sentinel</code> extension을 읽어 schema artifact를 SSOT로 검증한다. 공개 런타임 SSOT도 재확인해 custom domain asset을 <code>assets/index-DgN8JVBw.js</code>/<code>assets/index--s3xqgO5.css</code>로 갱신했고, OAuth start는 동작하지만 인증된 <code>/turn</code> proof는 여전히 owner storageState gate로 남겼다. <code>vnet.18ka.net</code>/<code>api-vnet.18ka.net</code> DNS A record는 0건이라 DONE 처리하지 않는다.</p>
|
||||
<p class="dg-note">85차 적용(2026-06-29): refactor-governance P3로 Node 전환 계약 검증의 <code>gateway-default</code> sentinel 중복을 줄였다. <code>scripts/check-engine-gateway-contract.mjs</code>는 더 이상 sentinel 문자열을 자체 상수로 들고 있지 않고, <code>engine_gateway_schema.v1.json</code>의 <code>x-engine-gateway-default-model-sentinel</code> extension을 읽어 schema artifact를 SSOT로 검증한다. 공개 런타임 SSOT도 재확인해 custom domain asset을 <code>assets/index-Cz4PBoO3.js</code>/<code>assets/index-DtVTvS9j.css</code>로 갱신했고, OAuth start는 동작하지만 인증된 <code>/turn</code> proof는 여전히 owner storageState gate로 남겼다. <code>vnet.18ka.net</code>/<code>api-vnet.18ka.net</code> DNS A record는 0건이라 DONE 처리하지 않는다.</p>
|
||||
<p class="dg-note">86차 적용(2026-06-29): refactor-governance P2/P3로 Session mic capture를 <code>AudioWorklet</code>-first 구조로 전환하고 기존 <code>MediaRecorder</code>를 fallback으로 남겼다. Worklet은 mono PCM16 chunk를 <code>/voice/ws</code> binary frame으로 보내고, control packet은 <code>format=pcm</code>, <code>sample_rate</code>, <code>channels</code>, <code>sample_width</code>를 명시한다. 백엔드는 <code>pcm</code> 업로드를 STT 호출 직전 RIFF/WAV로 정규화해 raw PCM을 <code>audio/wav</code>로 오인하던 계약 틈을 닫았다. 실제 Deepgram streaming, 물리 마이크, public WSS, 50분 양방향 실측은 계속 외부/실측 gate다. 검증: <code>app/test_voice_ws.py</code> 11 passed, <code>npm run typecheck</code>, <code>voice-success</code> chromium-single-run 2 passed.</p>
|
||||
<p class="dg-note">87차 적용(2026-06-29): refactor-governance P3로 <code>/voice/ws</code>에 provider-neutral <code>stt_result</code> control을 추가해 EOT helper를 route 경계에 연결했다. 새 control은 <code>text</code>, <code>final</code>, <code>silence_ms</code>, <code>provider_events</code>를 받아 <code>assess_end_of_turn()</code>이 ready를 판단하고, pending이면 턴 실행 없이 <code>state=listening</code>으로 돌아간다. ready일 때만 transcript를 emit하고 <code>_run_turn_and_speak()</code>로 진행한다. 기존 <code>audio_start</code>→binary→<code>audio_end</code>, <code>text_turn</code>, ping/close 계약은 유지했다. 실제 Deepgram interim/final WSS 연결과 public WSS/물리 마이크/50분 실측은 여전히 남은 gate다. 검증: <code>py_compile</code>, <code>app/test_voice_ws.py app/test_voice_service.py</code> 27 passed.</p>
|
||||
<p class="dg-note">88차 적용(2026-06-29): refactor-governance P3로 Node conformance runner의 stream event/payload 검증을 schema-driven으로 바꿨다. <code>scripts/check-engine-gateway-contract.mjs</code>는 더 이상 <code>token/done/error</code> 이벤트 목록과 payload 필드를 로컬 상수·분기로 들고 있지 않고, <code>engine_gateway_schema.v1.json</code>의 <code>StreamPacket.oneOf</code>, payload <code>$ref</code>, <code>x-engine-gateway-sse-events</code>를 읽어 golden fixture를 검증한다. raw token text fallback과 <code>data: [DONE]</code> compatibility line은 유지했다. 검증: <code>node scripts\check-engine-gateway-contract.mjs --json</code>, 당시 gateway/backend focused suite 통과, 이후 최신 L1 focused 120 passed로 재검증.</p>
|
||||
|
|
@ -548,6 +548,7 @@
|
|||
<p class="dg-note">113차 적용(2026-07-01): 학습자 <code>/learn/practice</code> 페르소나 리스트가 긴 한글 주호소 요약에서 카드 내부를 클립하던 문제를 수정했다. <code>.lh-list-pane</code>의 viewport 고정 높이와 내부 스크롤을 풀고, <code>.lh-personas</code>/<code>.lh-persona</code>가 콘텐츠 높이를 그대로 만들게 했으며, <code>.lh-persona__summary</code> 3줄 clamp를 제거했다. <code>learner.spec.ts</code>에는 각 페르소나 카드의 summary/body/card scroll overflow가 0인지 재는 regression guard를 추가했다. 검증: <code>npm run typecheck</code>, <code>npm run build</code>, 브라우저 rect smoke 1095px/1440px에서 카드 overflow 0 및 문서 가로 overflow 0. 현재 로컬 <code>127.0.0.1:8000/health</code>가 timeout이라 real API 기반 전체 <code>learner.spec.ts</code>는 이번 패스에서 미실행.</p>
|
||||
<p class="dg-note">114차 적용(2026-07-01): 회기 종료 후 background deep 평가가 실제 durable DB row로 저장되는지 fixture 없이 검증했다. <code>_schedule_session_evaluation()</code>은 생성한 task를 반환하고 done callback에서 crash/cancel을 로그로 관찰하며, 평가 결과·timeout/error 저장이 durable store에 닿지 않으면 명시 error 로그를 남긴다. 교수자 명시 재평가 API도 평가 산출물이 생성됐지만 저장 실패하면 503으로 표면화한다. <code>GET /eval/sessions/{id}/evaluation</code>은 <code>status/error/durable</code>을 노출해 캐시 착시와 DB 저장 증거를 구분한다. 새 DB-backed E2E는 학습자 세션 생성→턴→종료→teacher 로그인→<code>/eval/.../evaluation</code> polling→교수자 리뷰 UI <code>평가 완료</code>까지 실제 API/DB/엔진으로 확인한다. 검증: API 전체 309 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1</code> 7 passed, <code>PLAYWRIGHT_PORT=5203 npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1</code> 16 passed, 현재 Playwright 수집 기준 <code>162 tests in 18 files</code>.</p>
|
||||
<p class="dg-note">115차 적용(2026-07-01): 교수자 학생 분석을 <code>/teach</code> 대시보드에서 분리해 좌측 메뉴의 <code>/teach/analysis</code> 전용 작업면으로 이동했다. 교수 콘솔은 검토 큐·위기 알림·페르소나 검수·최근 회기 triage만 맡고, 학생 분석은 학습자 목록 레일, 사용자별 추이, 단계 분포, 전체 회기 타임라인, 회기 리뷰 드릴다운 구조를 맡는다. 회귀 방지로 <code>teacher.spec.ts</code>는 콘솔에 분석 패널이 없고 메뉴를 통해 학생 분석 페이지로 이동하는지 확인하며, <code>layout-visual-gate.spec.ts</code>는 학생 분석 페이지를 7개 폭에서 별도 캡처·검사한다. 검증: <code>npm run typecheck</code>, <code>npm run build</code>, <code>npm run check:api-types</code>, <code>PLAYWRIGHT_PORT=5211 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1</code> 9 passed, <code>PLAYWRIGHT_PORT=5214 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1</code> 11 passed, SSOT checker PASS, 현재 Playwright 수집 기준 163 tests / 18 files.</p>
|
||||
<p class="dg-note">116차 적용(2026-07-01): 원천문서 갭 E2E 목표에 맞춰 C1/C2 silent failure를 추가로 닫았다. C1은 학습자가 저장한 사례개념화 워크시트가 <code>app.case_worksheet</code>를 거쳐 교수자 검토 화면 read-only 값으로 복원되고, 교수자 <code>수정요청</code> 검수 메모가 <code>teacherReview.worksheetStatus</code>/<code>worksheetNote</code>로 재조회되는지 실제 브라우저/API/DB E2E로 고정했다. C2는 <code>app.safety_events</code>가 runtime bootstrap/healthcheck에서 빠져 DB 정상처럼 보일 수 있던 구멍과, <code>jsonb</code> codec 위에 이미 직렬화한 문자열을 넘겨 detail이 JSON string으로 저장되던 구멍을 수정했다. <code>record_safety_event()</code>는 RLS용 <code>ai_context=true</code>로 저장하고, alert list 실패는 non-dev에서 fail-closed한다. 검증: <code>python -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py app/test_teacher_dashboard.py app/test_runtime_policy.py -q</code> 59 passed, <code>PLAYWRIGHT_PORT=5228 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "case worksheet|crisis safety"</code> 2 passed, 현재 Playwright 수집 기준 <code>166 tests in 18 files</code>.</p>
|
||||
<p class="dg-note">20차 적용(2026-06-28): 신규 Google/SAML 사용자는 <code>account_status=pending</code>으로 시작하고 승인 전에는 <code>/pending</code> 안내 화면만 본다. <code>yunchan@twentyoz.kr</code>는 슈퍼 관리자 allowlist로 admin+approved를 받으며, <code>/admin/users</code>는 가입 승인 탭에서 pending 계정을 승인 또는 보류 처리한다.</p>
|
||||
<p class="dg-note">21차 적용(2026-06-28): 관리자 페이지 진입권을 기본 역할과 분리해 <code>app_user.admin_access</code>로 저장한다. <code>AUTH_SUPER_ADMIN_EMAILS</code> 기본값은 <code>yunchan@twentyoz.kr</code>, <code>hoonjungkoo@hs.ac.kr</code>이며, 슈퍼 관리자는 학습자·교수자·관리자 공간 전환과 관리자 권한 부여/회수를 할 수 있다. 학생·교수 계정도 <code>admin_access=true</code>면 우측 상단 관리자 진입이 노출된다. 구성 슈퍼 관리자의 권한 회수와 계정 비활성화는 차단한다.</p>
|
||||
<p class="dg-note">25차 적용(2026-06-28): <code>/admin/users</code>에서 허용 도메인 밖 이메일도 정확한 계정 단위로 강제 등록할 수 있다. Google/SAML/dev-login은 미리 등록된 이메일만 도메인 게이트 예외로 통과시키고, provider 로그인 시 기존 관리 row의 역할·코호트·승인 상태를 이어받는다. 미등록 외부 도메인 로그인은 계속 차단한다.</p>
|
||||
|
|
@ -918,7 +919,7 @@
|
|||
<thead><tr><th>범위</th><th>명령 / 확인 대상</th><th>결과</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Web typecheck</td><td><code>npm run typecheck</code></td><td>Passed</td></tr>
|
||||
<tr><td>Full Playwright E2E baseline</td><td><code>npx playwright test --list</code></td><td>현재 수집 기준 162 tests / 18 files. 최신 변경은 focused gate 행으로 별도 검증한다.</td></tr>
|
||||
<tr><td>Full Playwright E2E baseline</td><td><code>npx playwright test --list</code></td><td>현재 수집 기준 166 tests / 18 files. 최신 변경은 focused gate 행으로 별도 검증한다.</td></tr>
|
||||
<tr><td>API typegen SSOT</td><td><code>npm run check:api-types</code></td><td>Passed; FastAPI OpenAPI → <code>src/lib/api.gen.ts</code> stale check</td></tr>
|
||||
<tr><td>Contract SSOT aggregate DTO</td><td><code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_evaluation_persistence.py app/test_evaluator_model_routing.py app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_session_turn_persistence.py -q</code> / <code>npm run check:api-types</code> / <code>npm run typecheck</code> / <code>npm run build</code></td><td>50 backend passed; learner sessions, session review/worksheet, teacher dashboard, session start/detail DTOs use generated <code>ApiSchema</code> aliases with UI fallback. Stage responses are OpenAPI enum unions, including review phase key/label, reached phase, evaluation summary/trigger responses, and teacher session/growth stage. <code>.github/workflows/api-contract.yml</code> runs <code>npm run check:api-types</code> on API/Web contract changes.</td></tr>
|
||||
<tr><td>SEO/GEO share cards</td><td><code>pytest app/test_session_share.py app/test_session_turn_persistence.py -q</code> / <code>npm run generate:api-types</code> / <code>npm run typecheck</code></td><td>21 passed; session share creates hashed-token public unfurl payload without raw transcript, revoked token returns 404, OpenAPI generated share DTOs, review screen share button typechecks. Static <code>robots.txt</code>/<code>sitemap.xml</code>/<code>llms.txt</code> added.</td></tr>
|
||||
|
|
@ -931,10 +932,10 @@
|
|||
<tr><td>L1 session read-model split</td><td><code>python -B -m py_compile app/session_read_model.py app/routes/sessions.py</code> / <code>python -B -m pytest -p no:cacheprovider app/test_session_turn_persistence.py app/test_evaluation_persistence.py app/test_session_share.py app/test_learner_dashboard.py app/test_rbac_idor.py app/test_teacher_dashboard.py -q</code> / <code>npm run check:api-types</code> / <code>npm run typecheck</code></td><td>py_compile passed; focused backend 48 passed; OpenAPI generated API type check and web typecheck passed. <code>app/session_read_model.py</code> now owns session list/dashboard/detail/review/share DTOs and deterministic builders while <code>routes/sessions.py</code> keeps auth, RLS-backed reads, persistence, and route decorators.</td></tr>
|
||||
<tr><td>L1 persona read/generation split</td><td><code>py -3.11 -X utf8 -B -m py_compile app/persona_generation_contract.py app/persona_read_model.py app/routes/personas.py app/test_persona_generation_contract.py app/test_persona_review.py</code> / <code>py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_persona_generation_contract.py app/test_persona_review.py -q</code> / <code>npm run check:api-types</code> / <code>npm run typecheck</code></td><td>py_compile passed; persona contract+review 39 passed; OpenAPI generated API type check and web typecheck passed. <code>app/persona_read_model.py</code> owns persona catalog/review/draft/source/evidence DTOs and mappers, while <code>app/persona_generation_contract.py</code> owns draft schema, prompt bundle, <code>GenerateResponse</code> payload extraction, and generated draft defaults/coercion. <code>routes/personas.py</code> keeps auth, teacher/admin gates, DB repository calls, RAG source registration, engine invocation, provenance assembly, and HTTP error mapping.</td></tr>
|
||||
<tr><td>M1 provider_events preservation</td><td><code>python -X utf8 -m pytest -p no:cacheprovider app/test_runtime_policy.py app/test_session_turn_persistence.py app/test_voice_ws.py app/test_voice_service.py -q</code></td><td>77 passed; voice control/STT provider events are allowlisted, size-limited, and tagged with internal <code>event_type</code>/<code>category</code> taxonomy into <code>app.turns.provider_events</code>. 인증된 review <code>nonverbal</code>은 정규화 taxonomy 일부만 파생 칩으로 노출하며 raw transcript/text/provider/source/raw type은 응답하지 않는다. Runtime readiness now requires all five turn voice metadata columns.</td></tr>
|
||||
<tr><td>C1 case worksheet</td><td><code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_case_worksheet_rubric.py -q</code> / <code>scripts/check-case-worksheet-rubric.py --json</code> / <code>npm run check:api-types</code> / <code>npm run typecheck</code> / <code>npm run build</code> / <code>session-review.spec.ts</code> / local API smoke</td><td>40 passed for worksheet persistence/RBAC/teacher review plus rubric scaffold; session review exposes transcript-grounded <code>caseWorksheet</code>, learner-edited <code>saved_by_learner</code> worksheet persists through <code>app.case_worksheet</code>, teacher/admin <code>teacherReview</code> exposes manual <code>worksheetStatus</code>/<code>worksheetNote</code>/<code>worksheetReviewedAt</code>, and <code>case-worksheet-rubric@scaffold-2026-06-28</code> validates 5 sections/28 items with <code>scoring_enabled=false</code>. Local smoke returned <code>saved_by_learner:local smoke saved worksheet</code>. Stage labels are normalized at response boundaries.</td></tr>
|
||||
<tr><td>C1 case worksheet</td><td><code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_case_worksheet_rubric.py -q</code> / <code>scripts/check-case-worksheet-rubric.py --json</code> / <code>PLAYWRIGHT_PORT=5228 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "case worksheet"</code> / local API smoke</td><td>40 passed for worksheet persistence/RBAC/teacher review plus rubric scaffold; session review exposes transcript-grounded <code>caseWorksheet</code>, learner-edited <code>saved_by_learner</code> worksheet persists through <code>app.case_worksheet</code>, teacher/admin <code>teacherReview</code> exposes manual <code>worksheetStatus</code>/<code>worksheetNote</code>/<code>worksheetReviewedAt</code>, and full browser+API+DB E2E now verifies learner save → teacher read-only review → <code>수정요청</code> persistence. <code>case-worksheet-rubric@scaffold-2026-06-28</code> still validates 5 sections/28 items with <code>scoring_enabled=false</code>.</td></tr>
|
||||
<tr><td>M3 auth claim mapping</td><td><code>python -B -m pytest -p no:cacheprovider app/test_auth_providers.py -q</code></td><td>30 passed; Google/SAML cohort maps, provider external_id, provider error reason, PKCE/state, HttpOnly cookie, SAML fixture ACS covered</td></tr>
|
||||
<tr><td>Auth managed-user upsert boundary</td><td><code>py_compile</code> / <code>py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_auth_providers.py app/test_admin_ops.py app/test_rbac_idor.py app/test_runtime_policy.py app/test_session_turn_persistence.py app/test_learner_dashboard.py app/test_teacher_dashboard.py -q</code> / <code>npm run check:api-types</code></td><td><code>ManagedUserUpsertInput</code> owns managed-user create/reactivate inputs; <code>create_session()</code>, admin user create, and direct auth regression calls no longer pass long keyword bags. <code>ManagedUserMemoryInput</code> remains DB→memory fallback/store sync, and <code>ManagedUserPatch</code> remains partial profile/admin update. Focused backend 103 passed; API type drift check passed.</td></tr>
|
||||
<tr><td>CrisisGate 109</td><td><code>python -m pytest app/test_session_turn_persistence.py app/test_voice_ws.py -q</code></td><td>20 passed; real crisis stops before engine, returns 109 resource, safety event DB insert payload covered</td></tr>
|
||||
<tr><td>CrisisGate 109</td><td><code>python -m pytest app/test_session_turn_persistence.py app/test_voice_ws.py -q</code> / <code>PLAYWRIGHT_PORT=5228 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "crisis safety"</code></td><td>real crisis stops before engine, returns 109 resource, and DB insert uses <code>ai_context=true</code>. <code>db.healthcheck()</code> now requires <code>app.safety_events</code>; alert detail handles legacy double-encoded JSON string rows; full browser+API+DB E2E verifies <code>safety_events</code> reaches the DB-backed teacher safety queue with resource 109.</td></tr>
|
||||
<tr><td>Professor review workflow</td><td><code>python -X utf8 -m pytest -p no:cacheprovider app/test_teacher_dashboard.py -q</code> / <code>PLAYWRIGHT_PORT=5211 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1</code> / <code>PLAYWRIGHT_PORT=5214 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1</code> / <code>npm run check:api-types</code> / <code>npm run typecheck</code> / <code>npm run build</code></td><td>5 backend passed + 9 E2E passed + strict visual gate 11 passed; <code>/teach</code> now keeps only triage queues, and <code>/teach/analysis</code> returns a selected learner's full session timeline, unrestricted trend points, stage breakdown, and review counts in a separate student analysis page.</td></tr>
|
||||
<tr><td>Learner/teacher growth metrics</td><td><code>py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_evaluation_persistence.py app/test_evaluator_model_routing.py app/test_session_turn_persistence.py app/test_rbac_idor.py app/test_teacher_dashboard.py app/test_learner_dashboard.py -q</code></td><td>59 backend passed; shared <code>session_metrics</code> now prefers rehydrated <code>label_ko</code> for technique labels while preserving <code>label/name/id/code</code> fallback. <code>alternative_utterances</code> string and <code>text/suggestion</code> dict inputs are pinned before any future evaluator adapter extraction.</td></tr>
|
||||
<tr><td>Teacher review status</td><td><code>pytest app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_learner_dashboard.py -q</code> / <code>npm run typecheck</code> / <code>npm run build</code> / <code>teacher.spec.ts</code></td><td>12 backend passed + 4 E2E passed; <code>app.session_review_status</code>, teacher dashboard <code>review_status/review_note/reviewed_at</code>, worksheet <code>worksheet_status/worksheet_note/worksheet_reviewed_at</code>, and <code>PUT /teacher/sessions/{session_id}/review-status</code> are documented. 검토 큐 visible 회귀와 교수자 전용 review grid 영역도 테스트로 고정했다.</td></tr>
|
||||
|
|
@ -946,7 +947,7 @@
|
|||
<tr><td>P1/H4 masking gate</td><td><code>app/test_pii_masking_eval.py app/test_orchestrator_masking.py app/test_evaluation_persistence.py app/test_session_turn_persistence.py</code></td><td>47 passed; 최신 보정 회귀 59 passed. phone/email/RRN 및 한국어 NAME/ORG raw 값이 generate/stream/evaluator payload와 client <code>text_masked</code>에 남지 않음. Optional ko recognizer fake span은 <code>[NAME]</code>/<code>[ORG]</code>로 마스킹되고 같은 문장의 phone은 후단 regex가 처리하며, adapter 실패 시에도 regex fallback이 유지된다. Synthetic ko fixture 16/16 pass, 자연 발화형 이름 라벨·자기소개와 negative control 포함, input/report schema validation, summary-only report(<code>evidence_text_included=false</code>), entity recall 1.0, forbidden substring removal 1.0, unexpected entity violations 0.</td></tr>
|
||||
<tr><td>P2a RBAC/audit/visibility</td><td><code>app.test_rbac_idor</code></td><td>11 tests OK; other learner 403, read_session audit, evaluator-only hidden, teacher session review read allowed, super_admin learner review principal is promoted to admin for evaluation/worksheet/review-status loaders, admin_access-only learner remains blocked, and learner worksheet write remains 403</td></tr>
|
||||
<tr><td>Admin access delegation</td><td><code>python -B -m pytest -p no:cacheprovider app/test_auth_providers.py app/test_runtime_policy.py app/test_rbac_idor.py app/test_teacher_dashboard.py app/test_learner_dashboard.py -q</code> / <code>npx playwright test e2e/admin.spec.ts --project=chromium-desktop --workers=1</code> / <code>npm run check:api-types</code> / <code>npm run typecheck</code> / <code>npm run build</code></td><td>70 backend focused passed + admin/auth/runtime 64 passed + admin E2E 8 passed. 실제 <code>admin</code> role은 학습자·교수자·관리자 공간을 모두 열고, <code>admin_access</code>만 받은 비관리자 계정은 관리자 콘솔 진입권만 얻는다. 관리자 sidebar는 운영 홈/사용자/권한/티켓과 교수자·학습자 작업 공간 링크를 함께 노출한다.</td></tr>
|
||||
<tr><td>Session read-model DB readiness</td><td><code>python scripts\check-deploy-preflight.py --skip-db --env-file infra\.env.example --allow-placeholder-secrets</code> / <code>https://api-vignette.chanpaca.net/health</code></td><td>2026-06-29 prod 503 원인은 운영 DB의 <code>app.turns.provider_events</code> 컬럼 누락이었다. 운영 DB hotfix 후 <code>/teacher/dashboard</code> code path는 <code>source=database</code>로 복구됐다. 현재 <code>db.healthcheck()</code>, runtime table readiness, deploy preflight DB mode는 <code>app.turns</code> 음성 메타 컬럼 5개(<code>audio_ref</code>, <code>silence_ms</code>, <code>speech_rate</code>, <code>barge_in</code>, <code>provider_events</code>)와 <code>app.session_review_status</code> worksheet 컬럼을 함께 검증한다. 2026-06-30에는 Docker Desktop/DB 중단으로 public API 530/error code 1033이 재발했지만, Docker Desktop/DB 재기동 뒤 public health가 <code>environment=prod</code>, <code>db=true</code>, <code>engine=true</code>로 복구됐다.</td></tr>
|
||||
<tr><td>Session read-model DB readiness</td><td><code>python scripts\check-deploy-preflight.py --skip-db --env-file infra\.env.example --allow-placeholder-secrets</code> / <code>https://api-vignette.chanpaca.net/health</code></td><td>2026-06-29 prod 503 원인은 운영 DB의 <code>app.turns.provider_events</code> 컬럼 누락이었다. 운영 DB hotfix 후 <code>/teacher/dashboard</code> code path는 <code>source=database</code>로 복구됐다. 현재 <code>db.healthcheck()</code>, runtime table readiness, deploy preflight DB mode는 <code>app.turns</code> 음성 메타 컬럼 5개(<code>audio_ref</code>, <code>silence_ms</code>, <code>speech_rate</code>, <code>barge_in</code>, <code>provider_events</code>), <code>app.session_review_status</code> worksheet 컬럼, <code>app.safety_events</code> 필수 컬럼을 함께 검증한다. 2026-06-30에는 Docker Desktop/DB 중단으로 public API 530/error code 1033이 재발했지만, Docker Desktop/DB 재기동 뒤 public health가 <code>environment=prod</code>, <code>db=true</code>, <code>engine=true</code>로 복구됐다.</td></tr>
|
||||
<tr><td>Admin usage persistence</td><td><code>python -m pytest app/test_admin_ops.py app/test_runtime_policy.py -q</code> / authenticated local public-API smoke</td><td>26 passed; <code>/admin/usage</code> returns 200 with <code>source=database</code>, <code>durable=true</code>. Cloudflare blocked raw Python public smoke with 1010, so app-level HTTP was verified against <code>127.0.0.1:8001</code> using the same prod process.</td></tr>
|
||||
<tr><td>Synthetic health sampler</td><td><code>record_admin_health_sample()</code>, <code>record-admin-health-sample.py</code>, <code>install-health-sampler-task.ps1</code></td><td>Backend focused 31 passed. Python compile/help passed; PowerShell parser + <code>-PrintOnly</code> passed. Local one-shot appended 5 service rows to <code>app.admin_health_event</code>: status ok, engine_mode claude_cli. This remains sample history, not an SLA claim.</td></tr>
|
||||
<tr><td>Health retention/rollup</td><td><code>admin_health_daily_rollup</code>, <code>maintain-admin-health-events.py</code>, <code>/admin/uptime</code></td><td>Backend focused 36 passed. Python compile/help passed. Local dry-run returned <code>rollup_event_count=0</code>, <code>prunable_event_count=0</code>. Uptime summary now combines raw samples with daily rollups when raw rows have been pruned; detailed events stay raw-only.</td></tr>
|
||||
|
|
|
|||
|
|
@ -51,8 +51,8 @@
|
|||
|
||||
| ID | 갭 | 현재상태 | 권고 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
| **C1** | 사례개념화·치료계획 산출물 저장형 구조 4차 구현·루브릭 외부화 scaffold·수동 교수자 검수 골격 완료·채점/정책 잔여 | `SessionReviewResponse.caseWorksheet`가 축어록 근거 기반 초안을 제공하고, 리뷰 화면에서 학습자가 편집한 워크시트를 `PUT /sessions/{id}/review/worksheet`로 `app.case_worksheet`에 저장한다. 이후 `GET /review`는 `saved_by_learner` 저장본을 자동 초안보다 우선 반환한다. 3차에서는 `data/rubrics/case-worksheet-rubric.json`, Draft 2020-12 schema, `app.services.case_worksheet_rubric`, `scripts/check-case-worksheet-rubric.py`를 추가해 임상팀 확정 루브릭을 코드 수정 없이 받을 scaffold를 만들었다. 현재 rubric은 `scaffold_only`/`scoring_enabled=false`이며 서버 생성 워크시트 5개 section/28개 item key와 일치하는지만 검증한다. 템플릿 key source는 `CASE_WORKSHEET_SECTION_SPECS`와 `case_worksheet_template_item_keys()`로 명시해 validator/CLI/test가 더미 턴 없이 같은 생성 spec을 참조한다. 4차에서는 기존 `app.session_review_status`에 `worksheet_status`/`worksheet_note`/`worksheet_reviewed_at`을 붙이고, `PUT /teacher/sessions/{id}/review-status`와 교수자 `SessionReview` 카드에서 승인·수정요청·반려 수동 판정을 저장/표시한다. CCD는 숨은 정답키로 남고 저장본에 점수로 노출하지 않는다. | 임상팀 확정 루브릭 콘텐츠(level anchor, 가중치, 컷오프, 예시 답안), AI 채점 적용·calibration, 승인 후 잠금·재제출 정책, 재귀학습 데이터셋 approved 연계. | doc1·doc4·doc5 |
|
||||
| **C2** | 위기개입 프로토콜·생명유지서약·에스컬레이션 1차 배선 완료·임상 고도화 필요 | 실제 자해·자살 신호는 LLM/엔진 호출 전 중단하고 109 리소스와 `conversation_stopped`를 REST/SSE/voice 응답에 싣는다. `crisis.risk_level`은 상태머신 `ideation_observed`로 전달하고, escalate 시 `app.safety_events` detail 적재 및 교수자 대시보드 안전 알림 큐로 연결한다. | 남은 것은 임상팀 콘텐츠: 비밀보장 예외고지→단계적 탐색→생명유지서약 스크립트, 실시간 push/메일 알림 정책, 위기탐색 누락 시 회기리뷰 감점 루브릭. | doc1·doc5(핵심 시나리오)·doc4(IRB 전제) |
|
||||
| **C1** | 사례개념화·치료계획 산출물 저장형 구조 4차 구현·루브릭 외부화 scaffold·수동 교수자 검수 골격 완료·채점/정책 잔여 | `SessionReviewResponse.caseWorksheet`가 축어록 근거 기반 초안을 제공하고, 리뷰 화면에서 학습자가 편집한 워크시트를 `PUT /sessions/{id}/review/worksheet`로 `app.case_worksheet`에 저장한다. 이후 `GET /review`는 `saved_by_learner` 저장본을 자동 초안보다 우선 반환한다. 3차에서는 `data/rubrics/case-worksheet-rubric.json`, Draft 2020-12 schema, `app.services.case_worksheet_rubric`, `scripts/check-case-worksheet-rubric.py`를 추가해 임상팀 확정 루브릭을 코드 수정 없이 받을 scaffold를 만들었다. 현재 rubric은 `scaffold_only`/`scoring_enabled=false`이며 서버 생성 워크시트 5개 section/28개 item key와 일치하는지만 검증한다. 템플릿 key source는 `CASE_WORKSHEET_SECTION_SPECS`와 `case_worksheet_template_item_keys()`로 명시해 validator/CLI/test가 더미 턴 없이 같은 생성 spec을 참조한다. 4차에서는 기존 `app.session_review_status`에 `worksheet_status`/`worksheet_note`/`worksheet_reviewed_at`을 붙이고, `PUT /teacher/sessions/{id}/review-status`와 교수자 `SessionReview` 카드에서 승인·수정요청·반려 수동 판정을 저장/표시한다. 2026-07-01 E2E는 학습자 저장본이 교수자 read-only 검토 화면으로 복원되고 `수정요청` 검수 메모가 DB-backed `teacherReview`로 재조회되는지 검증한다. CCD는 숨은 정답키로 남고 저장본에 점수로 노출하지 않는다. | 임상팀 확정 루브릭 콘텐츠(level anchor, 가중치, 컷오프, 예시 답안), AI 채점 적용·calibration, 승인 후 잠금·재제출 정책, 재귀학습 데이터셋 approved 연계. | doc1·doc4·doc5 |
|
||||
| **C2** | 위기개입 프로토콜·생명유지서약·에스컬레이션 1차 배선 완료·임상 고도화 필요 | 실제 자해·자살 신호는 LLM/엔진 호출 전 중단하고 109 리소스와 `conversation_stopped`를 REST/SSE/voice 응답에 싣는다. `crisis.risk_level`은 상태머신 `ideation_observed`로 전달하고, escalate 시 `app.safety_events` detail 적재 및 교수자 대시보드 안전 알림 큐로 연결한다. 2026-07-01 E2E는 실제 브라우저 위기 발화가 `app.safety_events`를 거쳐 DB-backed 교수자 안전 큐의 109 알림으로 표시되는지 검증한다. `db.healthcheck()`는 이제 `app.safety_events` schema를 요구하고, JSONB detail double-encoding legacy row도 안전하게 읽는다. | 남은 것은 임상팀 콘텐츠: 비밀보장 예외고지→단계적 탐색→생명유지서약 스크립트, 실시간 push/메일 알림 정책, 위기탐색 누락 시 회기리뷰 감점 루브릭. | doc1·doc5(핵심 시나리오)·doc4(IRB 전제) |
|
||||
| **C3** | 이론모드 2차 배선·학습자 명시 선택 UI 완료·CBT 콘텐츠 잔여 | `TurnContext`/`prepare_turn`/sessions/voice/evaluator에 `theory_mode`가 전달되고, `build_turn_messages`도 인간중심·CBT·통합 이론 프레이밍을 엔진 메시지에 넣는다. 프론트는 `persona.theory_target` 기준 기본값을 잡되, 세션 시작 전 `humanistic`/`cbt`/`integrative` segmented control로 학습자가 명시 선택하고 `POST /sessions`의 `theory_mode`로 전송한다. | 임상팀이 확정한 CBT 단계 프롬프트 체인, 이론부합 채점 루브릭. 이번 UI는 임상 문안·루브릭·백엔드 enum을 확장하지 않았다. | doc4(humanistic+CBT 필수)·doc2/doc5 |
|
||||
|
||||
### High (4)
|
||||
|
|
@ -85,8 +85,8 @@
|
|||
|
||||
### A. 즉시 착수 가능 (내부 코드, 외부 합의 불요)
|
||||
|
||||
- **C1 4차 완료**: `caseWorksheet` 응답 구조, 리뷰 화면 편집 UI, `app.case_worksheet` 저장/재조회 경로, 외부 루브릭 schema/loader/validation scaffold, 교수자 수동 워크시트 검수 상태 저장/표시. 워크시트 템플릿 key source는 `CASE_WORKSHEET_SECTION_SPECS`/`case_worksheet_template_item_keys()`로 명시했다. 후속은 임상팀 확정 루브릭 콘텐츠, AI 채점 보정, 승인 후 잠금·재제출 정책.
|
||||
- **C2 1차 완료**: 위기 신호는 엔진 전 중단, 109 안내, `safety_events` 적재, 교수자 알림 큐, `ideation_observed` 전달까지 배선했다. 후속은 임상 스크립트·서약 문안·감점 루브릭.
|
||||
- **C1 4차 완료 + E2E 고정**: `caseWorksheet` 응답 구조, 리뷰 화면 편집 UI, `app.case_worksheet` 저장/재조회 경로, 외부 루브릭 schema/loader/validation scaffold, 교수자 수동 워크시트 검수 상태 저장/표시를 구현했고, 실제 브라우저/API/DB E2E가 learner-save→teacher-review→수정요청 재조회를 검증한다. 워크시트 템플릿 key source는 `CASE_WORKSHEET_SECTION_SPECS`/`case_worksheet_template_item_keys()`로 명시했다. 후속은 임상팀 확정 루브릭 콘텐츠, AI 채점 보정, 승인 후 잠금·재제출 정책.
|
||||
- **C2 1차 완료 + E2E 고정**: 위기 신호는 엔진 전 중단, 109 안내, `safety_events` 적재, 교수자 알림 큐, `ideation_observed` 전달까지 배선했다. 실제 브라우저/API/DB E2E가 crisis stream→`app.safety_events`→교수자 안전 큐 109를 검증한다. 후속은 임상 스크립트·서약 문안·감점 루브릭.
|
||||
- **C3 2차 완료**: `theory_mode`가 세션·평가·생성 프롬프트까지 흐르고, 학습자는 세션 시작 전 기존 3개 모드 중 하나를 명시 선택해 `POST /sessions`로 보낸다. 후속은 임상팀 CBT 체인·이론부합 루브릭.
|
||||
- **H2 1차+라이브 코칭+리뷰 3열 workbench 완료**: `make_eval_hook`과 stream 평가가 턴 파이프라인에 붙고 정규화 테이블로 적재·복원된다. 대안발화도 `app.alternative_utterance`로 정규화한다. 라이브 코칭은 0615 워크북·DSM·공식 지침 요약/RAG 근거로 코칭 아바타 말풍선·근거 모달·발화별 이력 오버레이까지 연결했다. `POST /kb/live-coach/source-packs/sync`는 공용 source pack sync service를 통해 evaluator 전용 RAG에 증분 색인하고, hash 변경 시 document version을 최신+1로 올린다. 관리자 sync/CLI는 source pack manifest 생성 전 process-local cache를 refresh해 현재 repo JSON 기준으로 비교한다. 학습자 `SessionReview` 데스크톱은 요약/흐름, 축어록, 평가 rail의 3열 workbench와 하단 워크시트로 재배치했고, 교수자/모바일 레이아웃은 기존 규칙을 유지한다. 후속은 골든셋·임상팀 루브릭·source pack 임상 검수 상태 운영.
|
||||
- **H3 10차 완료**: 페르소나 저작 CRUD(draft→review), P4~P7 저장소 JSON 로드, RAG source 기반 draft generation, prompt bundle id/version/hash provenance와 seed/version materializer runner가 붙었다. seed runner는 dry-run/JSON manifest를 제공하고, `--apply`에서만 DB pool을 초기화해 기존 materializer를 호출한다. repo-managed source pack runner는 DB-backed dry-run/apply를 제공하며 active `content_hash`가 바뀐 문서만 최신 version+1로 sync한다. `kb.raw_source_artifact` hash-only 레코드와 `rag.index_document()` raw/sensitivity=3 fail-closed guard를 추가해 raw 원문이 `kb.chunk`/embedding/FTS로 들어가지 않게 했고, `app/persona_read_model.py`로 persona DTO/mapper 경계를 분리했다. PersonaStudio의 자동사고·회기 시나리오·말투 목록·역린/금기 목록은 행 추가/삭제 UI로 바꾸고 기존 배열 payload 계약을 유지한다. 이번 패스에서 프롬프트 미리보기 raw JSON textarea를 라벨형 검토 섹션으로 교체했다. 후속은 루브릭·이론 콘텐츠 외부화, 임상팀 최종 검수 evidence, 암호화 blob/vault 기반 원문 실저장.
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ Vignette 저장소의 모든 검증 수단(백엔드 단위 테스트, 웹 타
|
|||
| API 타입 생성 체크 | `apps/web` | `npm run check:api-types` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
|
||||
| 웹 타입체크 | `apps/web` | `npm run typecheck` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
|
||||
| 웹 빌드 | `apps/web` | `npm run build` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
|
||||
| Playwright E2E(전체) | `apps/web` | `npm run e2e` | **필요(+시드)** | **필요** | 자동기동 | 일부만 | **필요** | 162 tests / 18 files |
|
||||
| Playwright E2E(전체) | `apps/web` | `npm run e2e` | **필요(+시드)** | **필요** | 자동기동 | 일부만 | **필요** | 166 tests / 18 files |
|
||||
|
||||
핵심 원칙: **단위 테스트(pytest)와 타입체크/빌드는 외부 서비스 없이 단독 실행된다.**
|
||||
**E2E만 풀스택(DB+API+웹+브라우저)을 요구한다.** 아래 각 절에서 근거와 절차를 설명한다.
|
||||
|
|
@ -258,14 +258,15 @@ VITE_API_BASE=http://127.0.0.1:8000 npm run e2e # 프록시 대신 API
|
|||
|
||||
### 3.6 실측 테스트 개수 (현재)
|
||||
|
||||
`npx playwright test --list` 기준 **총 162 tests / 18 files**.
|
||||
`npx playwright test --list` 기준 **총 166 tests / 18 files**.
|
||||
|
||||
- **병렬 시나리오**: 130 tests (desktop 65 + mobile 65)
|
||||
- **`@single-run` 직렬 시나리오**: 32 tests (DB 영속화·세션 MVP·음성 성공경로·회기말 평가 저장·교수자 사용자별 분석 등)
|
||||
- **`@single-run` 직렬 시나리오**: 36 tests (DB 영속화·세션 MVP·음성 성공경로·회기말 평가 저장·교수자 사용자별 분석 등)
|
||||
- `e2e/voice-success.spec.ts`는 직접 `/voice/ws` 캐스케이드와 Session 마이크 UI를 함께 검증하며,
|
||||
브라우저 `<audio>.play()`가 차단된 조건에서도 Web Audio buffer source 재생이 시작되는지와
|
||||
Session 마이크 UI가 `audio_end`에 browser voice activity/silence 메타를 싣는지 확인한다.
|
||||
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` **7 passed**. 실제 브라우저 `openSessionStream()` → `/sessions/{id}/stream` → DB-backed `/review` 축어록 저장 경로, AI 튜터 코칭 이력 저장/재로딩, WebSocket `stt_result` 음성 비언어 메타데이터, Session 마이크 UI가 생성한 voice activity/silence 메타, Phase 3 pre/post 점수의 DB-backed 저장/재조회, 그리고 세션 종료 background deep 평가가 durable DB row로 저장되어 교수자 리뷰가 `평가 완료`로 전환되는지 검증한다.
|
||||
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5228 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "case worksheet|crisis safety"` **2 passed**. C1 학습자 워크시트 저장→교수자 수정요청 검수와 C2 위기 신호→`app.safety_events`→DB-backed 교수자 안전 알림 큐를 실제 브라우저/API/DB로 검증한다.
|
||||
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5193 npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1` **2 passed**. MVP 종료/리뷰 흐름과 위기 안전 게이트가 빈 내담자 응답 신호에 덮이지 않는지 검증한다.
|
||||
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5211 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1` **9 passed**. 교수자 콘솔의 페르소나 검수, 검토 큐, 최근 회기 진입과 별도 `/teach/analysis` 학생 분석 메뉴, 전체 회기 타임라인 전환을 검증한다.
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@
|
|||
|
||||
최신 동기화 추가(2026-07-01): 교수자 학생 분석을 콘솔에서 분리했다. `GET /teacher/learners/{learner_id}/analysis`는 담당 범위 안의 특정 학습자 전체 회기를 오래된 순서로 반환하고, 제한 없는 회기별 추이 point, 라포·탐색·개입·정리 단계 분포, 검토 대기/완료 카운트를 함께 내려준다. `/teach`는 검토 큐·위기 알림·페르소나 검수·최근 회기 triage만 맡고, 좌측 메뉴의 `/teach/analysis`가 학습자 목록 레일 → 사용자별 분석 → 전체 회기 타임라인 → 회기 리뷰 드릴다운을 맡는다. 검증: `python -X utf8 -m pytest -p no:cacheprovider app/test_teacher_dashboard.py -q` 5 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `PLAYWRIGHT_PORT=5211 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1` 9 passed, `PLAYWRIGHT_PORT=5214 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 11 passed, `python -X utf8 scripts/check-dev-dashboard-ssot.py --json` PASS, `npx playwright test --list` 현재 163 tests / 18 files.
|
||||
|
||||
최신 동기화 추가(2026-07-01): 원천문서 갭 E2E 탐색을 이어서 H1/C3 silent failure 후보를 좁혔다. `session-persistence.spec.ts`는 Phase 3 pre/post 3척도 입력이 브라우저 리뷰 UI → `PUT /users/me/prepost-measures` → DB-backed `GET /users/me/prepost-measures` → 새로고침 후 UI 복원까지 실제 API/DB로 이어지는지 검증한다. C3 수동 재평가는 persona 기본 `theory_target`이 학습자가 선택한 session `theory_mode`를 덮어쓰던 우선순위 오류를 수정했고, turn 재평가도 `TurnContext.theory_mode`를 채우도록 회귀화했다. 서브에이전트 병렬 조사 결과 남은 큰 공백은 C1 워크시트 learner-save/teacher-review full browser+API+DB E2E, C2 safety_events→교수자 대시보드 E2E, H2 live-coach evidence modal/source-pack actual sync proof다. 검증: `python -X utf8 -m pytest -p no:cacheprovider app/test_eval_routes.py app/test_notifications.py app/test_session_turn_persistence.py -q` 39 passed, `PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` 7 passed, `npx playwright test --list` 현재 162 tests / 18 files.
|
||||
최신 동기화 추가(2026-07-01): 원천문서 갭 E2E 탐색을 이어서 H1/C3 silent failure 후보를 좁혔다. `session-persistence.spec.ts`는 Phase 3 pre/post 3척도 입력이 브라우저 리뷰 UI → `PUT /users/me/prepost-measures` → DB-backed `GET /users/me/prepost-measures` → 새로고침 후 UI 복원까지 실제 API/DB로 이어지는지 검증한다. C3 수동 재평가는 persona 기본 `theory_target`이 학습자가 선택한 session `theory_mode`를 덮어쓰던 우선순위 오류를 수정했고, turn 재평가도 `TurnContext.theory_mode`를 채우도록 회귀화했다. 서브에이전트 병렬 조사 결과 남은 큰 공백은 C1 워크시트 learner-save/teacher-review full browser+API+DB E2E, C2 safety_events→교수자 대시보드 E2E, H2 live-coach evidence modal/source-pack actual sync proof였다. 검증: `python -X utf8 -m pytest -p no:cacheprovider app/test_eval_routes.py app/test_notifications.py app/test_session_turn_persistence.py -q` 39 passed, `PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` 7 passed, `npx playwright test --list` 현재 162 tests / 18 files.
|
||||
|
||||
최신 동기화 추가(2026-07-01): 위 문단의 C1/C2 E2E 공백을 실제 브라우저/API/DB 검증으로 닫았다. C1은 학습자 사례개념화 워크시트 저장 → 교수자 검토 화면 read-only 복원 → `수정요청` 검수 메모 저장/재조회까지 `session-persistence.spec.ts`에 추가했다. C2는 `app.safety_events`가 runtime bootstrap/healthcheck에서 빠져 DB 정상처럼 보이던 readiness 공백과, `jsonb` codec 위에 이미 직렬화한 문자열을 넘겨 detail이 JSON string으로 저장되던 사일런트 오류를 수정했다. `record_safety_event()`는 RLS용 `ai_context=true`로 저장하고, 안전 알림 list 실패는 non-dev에서 fail-closed한다. 남은 큰 공백은 H2 live-coach evidence modal/source-pack actual sync proof다. 검증: `python -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py app/test_teacher_dashboard.py app/test_runtime_policy.py -q` 59 passed, `PLAYWRIGHT_PORT=5228 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "case worksheet|crisis safety"` 2 passed, `npx playwright test --list` 현재 166 tests / 18 files.
|
||||
|
||||
분류: **B1 비차단 폴리시** · **B2 환경 제약(증거 생산 불가)** · **B3 소유자 결정** · **B4 외부 거버넌스**
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue