개선관리 요구사항과 Google 로그인을 완료

This commit is contained in:
Yun Chan 2026-08-28 16:07:09 +09:00
parent cc0a15b7c6
commit 2a39636163
112 changed files with 10166 additions and 527 deletions

View file

@ -19,6 +19,7 @@ import "./alliance-pulse.css";
interface PulseTurn {
id: string;
turn_id?: string | null;
ts: string;
speaker: string;
who: string;
@ -29,6 +30,7 @@ interface AlliancePulseCardProps {
sessionId: string;
turns: PulseTurn[];
isSupervisorView: boolean;
feedbackEnabled: boolean;
onJumpToTurn: (turnId: string) => void;
}
@ -206,7 +208,16 @@ function EvidencePicker({
onChange: (turnIds: string[]) => void;
scope: "learner" | "supervisor";
}) {
const eligible = useMemo(() => turns.slice(-12), [turns]);
const eligible = useMemo(
() =>
turns
.flatMap((turn) => {
const persistedId = turn.turn_id?.trim();
return persistedId ? [{ turn, persistedId }] : [];
})
.slice(-12),
[turns],
);
const summaryLabel =
scope === "learner" ? "내 판단의 근거 장면 선택" : "교수자 판정 근거 장면 선택";
@ -226,12 +237,12 @@ function EvidencePicker({
</summary>
{eligible.length > 0 ? (
<div className="ap-evidence-picker__list">
{eligible.map((turn) => (
{eligible.map(({ turn, persistedId }) => (
<label key={turn.id}>
<input
type="checkbox"
checked={selected.includes(turn.id)}
onChange={() => toggle(turn.id)}
checked={selected.includes(persistedId)}
onChange={() => toggle(persistedId)}
/>
<span className="ap-evidence-picker__time">{turn.ts}</span>
<span>
@ -242,7 +253,7 @@ function EvidencePicker({
))}
</div>
) : (
<p> .</p>
<p> ID가 .</p>
)}
</details>
);
@ -360,6 +371,25 @@ function LockedWaitingState({ pulse }: { pulse: AlliancePulse }) {
);
}
function SelfOnlyState({ pulse }: { pulse: AlliancePulse }) {
return (
<div className="ap-waiting ap-self-only" aria-label="저장된 치료 동맹 자기평가">
<div className="ap-waiting__copy">
<b> </b>
<p> . AI· .</p>
</div>
<dl className="ap-locked-scores">
{DIMENSIONS.map((dimension) => (
<div key={dimension}>
<dt>{DIMENSION_COPY[dimension].short}</dt>
<dd>{scoreText(pulse.self_scores?.[dimension] ?? null)}</dd>
</div>
))}
</dl>
</div>
);
}
function ComparisonView({
pulse,
onJumpToTurn,
@ -525,6 +555,7 @@ export function AlliancePulseCard({
sessionId,
turns,
isSupervisorView,
feedbackEnabled,
onJumpToTurn,
}: AlliancePulseCardProps) {
const [loadState, setLoadState] = useState<LoadState>("loading");
@ -559,12 +590,16 @@ export function AlliancePulseCard({
}, [load]);
useEffect(() => {
if (!pulse || (pulse.status !== "awaiting_agents" && pulse.status !== "processing")) {
if (
!feedbackEnabled ||
!pulse ||
(pulse.status !== "awaiting_agents" && pulse.status !== "processing")
) {
return;
}
const timer = window.setTimeout(() => void load(), POLL_INTERVAL_MS);
return () => window.clearTimeout(timer);
}, [load, pulse]);
}, [feedbackEnabled, load, pulse]);
const submitSelfAssessment = async () => {
if (!isCompleteDraft(draft) || submitting) return;
@ -606,8 +641,8 @@ export function AlliancePulseCard({
const revealReady = Boolean(
pulse?.learner_locked_at && pulse?.revealed_at,
);
const status = pulse ? pulseStatusCopy(pulse) : null;
const learnerEvidenceRequired = turns.length > 0;
const status = feedbackEnabled && pulse ? pulseStatusCopy(pulse) : null;
const learnerEvidenceRequired = turns.some((turn) => Boolean(turn.turn_id?.trim()));
const canSubmit =
isCompleteDraft(draft) &&
(!learnerEvidenceRequired || evidenceTurnIds.length > 0) &&
@ -642,8 +677,16 @@ export function AlliancePulseCard({
content = (
<div className="ap-self-form">
<div className="ap-self-form__intro">
<b>AI </b>
<p> . .</p>
<b>
{feedbackEnabled
? "AI 관점을 보기 전에 먼저 스스로 판단합니다"
: "목표·과업·유대를 내가 직접 기록합니다"}
</b>
<p>
{feedbackEnabled
? "세 축을 따로 평가하세요. 제출하면 값은 잠기며 이후 관점에 맞춰 수정할 수 없습니다."
: "세 축을 따로 평가해 저장합니다. AI·교수자 파생 비교는 표시하지 않습니다."}
</p>
</div>
{DIMENSIONS.map((dimension) => (
<AxisAssessmentControl
@ -667,14 +710,22 @@ export function AlliancePulseCard({
<div className="ap-self-form__commit">
<span>
<Icon name="shield" size={15} strokeWidth={1.8} />
AI .
{feedbackEnabled
? "제출 뒤에는 AI 관점이 준비될 때까지 내 판단만 보입니다."
: "제출 뒤에도 내가 직접 잠근 기록만 표시합니다."}
</span>
<Button size="sm" disabled={!canSubmit} onClick={() => void submitSelfAssessment()}>
{submitting ? "판단 잠그는 중" : "내 판단 잠그고 관점 비교"}
{submitting
? "판단 잠그는 중"
: feedbackEnabled
? "내 판단 잠그고 관점 비교"
: "내 판단 기록"}
</Button>
</div>
</div>
);
} else if (!feedbackEnabled) {
content = <SelfOnlyState pulse={pulse} />;
} else if (!revealReady) {
content = <LockedWaitingState pulse={pulse} />;
} else {
@ -697,8 +748,14 @@ export function AlliancePulseCard({
<Card className="sr-card ap-card" aria-busy={loadState === "loading"}>
<div className="ap-card__head">
<div>
<Kicker dot={false}> </Kicker>
<h2>, , </h2>
<Kicker dot={false}>
{feedbackEnabled ? "치료 동맹 펄스" : "치료 동맹 자기평가"}
</Kicker>
<h2>
{feedbackEnabled
? "목표, 과업, 유대를 따로 봅니다"
: "목표, 과업, 유대를 직접 기록합니다"}
</h2>
</div>
{status ? <Badge tone={status.tone}>{status.label}</Badge> : null}
</div>