사례개념화 워크시트 저장 추가
This commit is contained in:
parent
7f4a2d90c7
commit
bd389a97cc
15 changed files with 614 additions and 42 deletions
|
|
@ -18,6 +18,7 @@ import {
|
|||
type ReviewPoint,
|
||||
type ReviewTechnique,
|
||||
type ReviewTurn,
|
||||
type ReviewWorksheetSection,
|
||||
type SessionReviewResponse,
|
||||
} from "../lib/api";
|
||||
import { ValenceChart } from "./session-review/ValenceChart";
|
||||
|
|
@ -130,21 +131,102 @@ function confidenceLabel(confidence: "none" | "low" | "medium") {
|
|||
return "빈칸";
|
||||
}
|
||||
|
||||
function cloneWorksheetSections(sections: ReviewWorksheetSection[]): ReviewWorksheetSection[] {
|
||||
return sections.map((section) => ({
|
||||
...section,
|
||||
items: (section.items ?? []).map((item) => ({
|
||||
...item,
|
||||
evidence: [...(item.evidence ?? [])],
|
||||
})),
|
||||
}));
|
||||
}
|
||||
|
||||
function CaseWorksheetCard({
|
||||
sessionId,
|
||||
worksheet,
|
||||
onJump,
|
||||
onSaved,
|
||||
}: {
|
||||
sessionId: string;
|
||||
worksheet: ReviewCaseWorksheet | null | undefined;
|
||||
onJump: (id: string) => void;
|
||||
onSaved: (worksheet: ReviewCaseWorksheet) => void;
|
||||
}) {
|
||||
const sections = worksheet?.sections ?? [];
|
||||
const limitations = worksheet?.limitations ?? [];
|
||||
const [draftSections, setDraftSections] = useState<ReviewWorksheetSection[]>(() =>
|
||||
cloneWorksheetSections(sections),
|
||||
);
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
const isSaved = worksheet?.status === "saved_by_learner";
|
||||
|
||||
useEffect(() => {
|
||||
setDraftSections(cloneWorksheetSections(sections));
|
||||
setDirty(false);
|
||||
setSaveError(null);
|
||||
}, [worksheet]);
|
||||
|
||||
const updateValue = (sectionKey: string, itemKey: string, value: string) => {
|
||||
setDraftSections((current) =>
|
||||
current.map((section) =>
|
||||
section.key !== sectionKey
|
||||
? section
|
||||
: {
|
||||
...section,
|
||||
items: (section.items ?? []).map((item) =>
|
||||
item.key !== itemKey
|
||||
? item
|
||||
: {
|
||||
...item,
|
||||
value,
|
||||
confidence: value.trim() ? item.confidence === "none" ? "low" : item.confidence : "none",
|
||||
emptyReason: value.trim() ? null : item.emptyReason,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
);
|
||||
setDirty(true);
|
||||
};
|
||||
|
||||
const saveWorksheet = async () => {
|
||||
setSaving(true);
|
||||
setSaveError(null);
|
||||
try {
|
||||
const saved = await sessionApi.saveWorksheet(sessionId, {
|
||||
sections: draftSections,
|
||||
limitations,
|
||||
});
|
||||
onSaved(saved);
|
||||
setDirty(false);
|
||||
} catch {
|
||||
setSaveError("워크시트를 저장하지 못했습니다.");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="sr-card sr-card--side sr-card--worksheet">
|
||||
<Kicker>사례개념화 워크시트</Kicker>
|
||||
<div className="sr-ws-toolbar">
|
||||
<div>
|
||||
<Kicker>사례개념화 워크시트</Kicker>
|
||||
<span className="sr-ws-state">{isSaved ? "저장된 학습자 제출본" : "축어록 기반 자동 초안"}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
disabled={!sections.length || !dirty || saving}
|
||||
onClick={saveWorksheet}
|
||||
>
|
||||
{saving ? "저장 중" : dirty ? "저장" : "저장됨"}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="sr-worksheet">
|
||||
{sections.length > 0 ? (
|
||||
sections.map((section) => (
|
||||
{draftSections.length > 0 ? (
|
||||
draftSections.map((section) => (
|
||||
<section className="sr-ws-section" key={section.key}>
|
||||
<h3>{section.title}</h3>
|
||||
<div className="sr-ws-items">
|
||||
|
|
@ -158,7 +240,15 @@ function CaseWorksheetCard({
|
|||
{confidenceLabel(item.confidence)}
|
||||
</span>
|
||||
</div>
|
||||
<p>{item.value || item.emptyReason || "근거 대기"}</p>
|
||||
<textarea
|
||||
className="sr-ws-input"
|
||||
value={item.value ?? ""}
|
||||
placeholder={item.emptyReason || "근거 대기"}
|
||||
rows={3}
|
||||
onChange={(event) =>
|
||||
updateValue(section.key, item.key, event.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
{evidence ? (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -181,6 +271,7 @@ function CaseWorksheetCard({
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
{saveError ? <div className="sr-ws-save-error">{saveError}</div> : null}
|
||||
{limitations.length > 0 ? (
|
||||
<div className="sr-ws-limitations">
|
||||
{limitations.slice(0, 2).map((item) => (
|
||||
|
|
@ -201,6 +292,10 @@ export default function SessionReview() {
|
|||
const [activeTurn, setActiveTurn] = useState<string | null>(null);
|
||||
const turnRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||
|
||||
const handleWorksheetSaved = (worksheet: ReviewCaseWorksheet) => {
|
||||
setData((current) => (current ? { ...current, caseWorksheet: worksheet } : current));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
if (!sessionId) {
|
||||
|
|
@ -613,7 +708,12 @@ export default function SessionReview() {
|
|||
) : null}
|
||||
</Card>
|
||||
|
||||
<CaseWorksheetCard worksheet={data.caseWorksheet} onJump={jumpToTurn} />
|
||||
<CaseWorksheetCard
|
||||
sessionId={data.session_id}
|
||||
worksheet={data.caseWorksheet}
|
||||
onJump={jumpToTurn}
|
||||
onSaved={handleWorksheetSaved}
|
||||
/>
|
||||
|
||||
<div className={`sr-feedback${data.clientFeedback ? " sr-feedback--filled" : ""}`}>
|
||||
<div className="sr-feedback__kicker">
|
||||
|
|
|
|||
|
|
@ -720,6 +720,23 @@
|
|||
padding-right: 4px;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
.sr-ws-toolbar {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--sp-3);
|
||||
}
|
||||
.sr-ws-toolbar > div {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
.sr-ws-state {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.sr-ws-section {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
|
|
@ -780,13 +797,27 @@
|
|||
color: var(--text-muted);
|
||||
background: var(--paper-2);
|
||||
}
|
||||
.sr-ws-item p {
|
||||
margin: 0;
|
||||
.sr-ws-input {
|
||||
width: 100%;
|
||||
min-height: 78px;
|
||||
resize: vertical;
|
||||
padding: 8px 9px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-body);
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--fs-xs);
|
||||
line-height: 1.55;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.sr-ws-input:focus {
|
||||
outline: 2px solid var(--accent-tint);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.sr-ws-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.sr-ws-evidence {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
|
|
@ -805,6 +836,12 @@
|
|||
.sr-ws-evidence:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.sr-ws-save-error {
|
||||
margin-top: var(--sp-3);
|
||||
color: var(--crit-text);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.sr-ws-limitations {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
|
|
@ -1106,6 +1143,13 @@
|
|||
.sr-worksheet {
|
||||
max-height: 460px;
|
||||
}
|
||||
.sr-ws-toolbar {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
.sr-ws-toolbar .vg-btn {
|
||||
width: 100%;
|
||||
}
|
||||
.sr-chart__plot {
|
||||
height: 156px;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue