전 저장소 리팩터링과 SSOT 정비
This commit is contained in:
parent
14ecbd4e7d
commit
3dfddcac6f
173 changed files with 19679 additions and 6952 deletions
168
apps/web/src/pages/session-review/model.ts
Normal file
168
apps/web/src/pages/session-review/model.ts
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import type {
|
||||
SessionReviewResponse,
|
||||
TeacherSessionReviewStatusResponse,
|
||||
UserPrepostMeasureItem,
|
||||
UserPrepostMeasuresResponse,
|
||||
} from "../../lib/api";
|
||||
|
||||
export type PrepostMeasureName = UserPrepostMeasureItem["measure_name"];
|
||||
export type PrepostTimepoint = UserPrepostMeasureItem["timepoint"];
|
||||
export type WorksheetReviewStatus =
|
||||
| "pending"
|
||||
| "approved"
|
||||
| "changes_requested"
|
||||
| "rejected";
|
||||
export type WorksheetDecisionStatus = Exclude<
|
||||
WorksheetReviewStatus,
|
||||
"pending"
|
||||
>;
|
||||
|
||||
export const PREPOST_PILOT_ID = "phase3-pilot-draft";
|
||||
export const PREPOST_INSTRUMENT_VERSION =
|
||||
"pilot-prepost-scaffold-2026-06-28";
|
||||
export const PREPOST_SCORE_MIN = 1;
|
||||
export const PREPOST_SCORE_MAX = 5;
|
||||
export const REVIEW_READY_POLL_INTERVAL_MS = 1500;
|
||||
export const REVIEW_READY_POLL_LIMIT = 60;
|
||||
|
||||
export const PREPOST_MEASURES: Array<{
|
||||
key: PrepostMeasureName;
|
||||
label: string;
|
||||
hint: string;
|
||||
}> = [
|
||||
{ key: "self_efficacy", label: "자기효능감", hint: "상담 수행 자신감" },
|
||||
{ key: "skill_proficiency", label: "기술숙련도", hint: "기법 적용 숙련감" },
|
||||
{
|
||||
key: "training_satisfaction",
|
||||
label: "수련만족도",
|
||||
hint: "훈련 경험 만족도",
|
||||
},
|
||||
];
|
||||
|
||||
export const PREPOST_TIMEPOINTS: Array<{
|
||||
key: PrepostTimepoint;
|
||||
label: string;
|
||||
}> = [
|
||||
{ key: "pre", label: "사전" },
|
||||
{ key: "post", label: "사후" },
|
||||
];
|
||||
|
||||
export function teacherReviewFromResponse(
|
||||
saved: TeacherSessionReviewStatusResponse,
|
||||
): NonNullable<SessionReviewResponse["teacherReview"]> {
|
||||
return {
|
||||
status: saved.status,
|
||||
note: saved.note,
|
||||
reviewerId: saved.reviewer_id ?? null,
|
||||
reviewedAt: saved.reviewed_at ?? null,
|
||||
updatedAt: saved.updated_at ?? null,
|
||||
worksheetStatus: saved.worksheet_status ?? "pending",
|
||||
worksheetNote: saved.worksheet_note ?? "",
|
||||
worksheetReviewedAt: saved.worksheet_reviewed_at ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function prepostDraftKey(
|
||||
measure: PrepostMeasureName,
|
||||
timepoint: PrepostTimepoint,
|
||||
) {
|
||||
return `${measure}:${timepoint}`;
|
||||
}
|
||||
|
||||
export function worksheetReviewStatusLabel(status: WorksheetReviewStatus) {
|
||||
if (status === "approved") return "승인";
|
||||
if (status === "changes_requested") return "수정요청";
|
||||
if (status === "rejected") return "반려";
|
||||
return "검수 대기";
|
||||
}
|
||||
|
||||
export function splitPrepostDraftKey(
|
||||
key: string,
|
||||
): [PrepostMeasureName, PrepostTimepoint] | null {
|
||||
const [measure, timepoint] = key.split(":");
|
||||
if (
|
||||
PREPOST_MEASURES.some((item) => item.key === measure) &&
|
||||
PREPOST_TIMEPOINTS.some((item) => item.key === timepoint)
|
||||
) {
|
||||
return [measure as PrepostMeasureName, timepoint as PrepostTimepoint];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function prepostScoreText(score: number) {
|
||||
return Number.isInteger(score)
|
||||
? String(score)
|
||||
: score.toFixed(1).replace(/\.0$/, "");
|
||||
}
|
||||
|
||||
export function prepostDraftFromResponse(
|
||||
response: UserPrepostMeasuresResponse | null,
|
||||
): Record<string, string> {
|
||||
const next: Record<string, string> = {};
|
||||
for (const item of response?.measures ?? []) {
|
||||
next[prepostDraftKey(item.measure_name, item.timepoint)] = prepostScoreText(
|
||||
item.raw_score,
|
||||
);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export function parsePrepostScore(value: string): number | null {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
const parsed = Number(trimmed);
|
||||
if (!Number.isFinite(parsed)) return null;
|
||||
if (parsed < PREPOST_SCORE_MIN || parsed > PREPOST_SCORE_MAX) return null;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function displayReviewSummary(summary: string) {
|
||||
if (
|
||||
/(?:사유:\s*)?(?:engine_error|session evaluation timeout|timeout|RuntimeError)/i.test(
|
||||
summary,
|
||||
)
|
||||
) {
|
||||
return "저장된 축어록은 확인했지만 deep-loop 평가 AI 산출물을 표시하지 못했습니다. AI 평가 재시도가 필요합니다.";
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
export function displayEvaluationRetryError(message: string) {
|
||||
if (!message.trim()) return "AI 평가를 다시 실행하지 못했습니다.";
|
||||
return "AI 평가 재시도를 완료하지 못했습니다. 잠시 뒤 다시 실행해 주세요.";
|
||||
}
|
||||
|
||||
const REVIEW_DISPLAY_PLACEHOLDERS: Record<string, string> = {
|
||||
"[NAME]": "익명 내담자",
|
||||
"[ORG]": "소속 기관",
|
||||
"[PHONE]": "연락처",
|
||||
"[EMAIL]": "이메일",
|
||||
"[RRN]": "주민등록번호",
|
||||
"[ADDRESS]": "주소",
|
||||
};
|
||||
|
||||
export function displayTranscriptText(text: string) {
|
||||
let next = text;
|
||||
for (const [placeholder, label] of Object.entries(
|
||||
REVIEW_DISPLAY_PLACEHOLDERS,
|
||||
)) {
|
||||
next = next.split(placeholder).join(label);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export function shouldPollReviewReady(data: SessionReviewResponse) {
|
||||
return (
|
||||
!data.reviewReady &&
|
||||
data.degraded &&
|
||||
data.supervisorState === "평가 대기" &&
|
||||
(data.turns?.length ?? 0) > 0
|
||||
);
|
||||
}
|
||||
|
||||
export function noteAuthorLabel(author: string) {
|
||||
const normalized = author.trim();
|
||||
if (normalized === "ai") return "AI";
|
||||
if (normalized === "transcript") return "기록";
|
||||
return normalized || "교수자";
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue