Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions apps/web/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CtrsLegend } from "../../components/CtrsLegend";
import { PatientListItem } from "../../components/PatientListItem";
import { APIException, listPatients, type PatientListItem as Item } from "../../lib/api";

Expand Down Expand Up @@ -49,13 +50,16 @@ export default async function DashboardPage() {
{patients.length === 0 ? (
<p className="text-text-secondary">아직 등록된 환자가 없어요.</p>
) : (
<ul className="flex flex-col gap-2.5">
{sorted.map((p) => (
<li key={p.userId}>
<PatientListItem item={p} />
</li>
))}
</ul>
<div className="flex flex-col md:flex-row gap-6 items-start">
<ul className="flex flex-col gap-2.5 flex-1 min-w-0 w-full">
{sorted.map((p) => (
<li key={p.userId}>
<PatientListItem item={p} />
</li>
))}
</ul>
<CtrsLegend />
</div>
)}
</div>
);
Expand Down
20 changes: 20 additions & 0 deletions apps/web/components/CtrsBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { CTRS_META, ctrsFromLevel, type RiskLevel } from "../lib/tokens";

/**
* 환자 목록용 CTRS(위기분류척도) 단계 배지 — `CTRS N · 라벨` 형식.
* safety risk level을 위험성(Rating A) 기준 CTRS 단계로 환산해 표기하며,
* RiskEvent가 없는 환자도 항상 `CTRS 5 · 안정`으로 표시한다(전원 단계 표기).
*/
export function CtrsBadge({ level }: { level: RiskLevel | null | undefined }) {
const stage = ctrsFromLevel(level);
const { label, badge } = CTRS_META[stage];
return (
<span
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full border text-[11px] font-semibold tracking-tight ${badge.bg} ${badge.border} ${badge.text}`}
aria-label={`CTRS ${stage}단계: ${label}`}
>
<span className={`h-1.5 w-1.5 rounded-full ${badge.dot}`} aria-hidden />
CTRS {stage} · {label}
</span>
);
}
73 changes: 73 additions & 0 deletions apps/web/components/CtrsLegend.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { CTRS_META, type CtrsStage } from "../lib/tokens";

/**
* CTRS(위기분류척도, Crisis Triage Rating Scale) 참고 범례.
* 환자 목록의 CTRS 단계 배지는 **위험성(Rating A)** 기준으로 자동 산출된다.
* 지지체계(Rating B)는 별도 축으로, 현재 자동 산출 대상은 아니며 임상 참고용.
*/

// Rating A: 위험성(Dangerousness) — 배지 단계와 직접 대응.
const RATING_A: Record<CtrsStage, string> = {
1: "자살·자해 사고/관련 환청, 현병력 중 자살시도, 예측 불가한 폭력·충동",
2: "자살 사고·행동으로 스스로 고통, 폭력·충동 과거력(현재 징후 없음)",
3: "자·타해 사고를 양가적으로 표현하거나 비효과적 몸짓만 있음",
4: "자·타해 사고/행동이 부분적·기왕력, 행동조절 욕구 분명·조절 가능",
5: "자·타해 사고나 행동의 과거력·위험 없음",
};

// Rating B: 지지체계(Support System) — 참고 축.
const RATING_B: Record<CtrsStage, string> = {
1: "가족·친구 등 지지체계 전무, 기관의 즉각 지지도 불가",
2: "동원 가능한 지지체계는 있으나 효과가 제한적",
3: "잠재적 지지체계는 있으나 제대로 기능하기 어려움",
4: "관심 있는 지지체계 있으나 지지 제공 능력·의지 다소 불명확",
5: "관심 있는 지지체계가 지지 제공 능력·의지를 갖춤",
};

const STAGES: CtrsStage[] = [1, 2, 3, 4, 5];

export function CtrsLegend() {
return (
<aside className="w-full md:w-72 shrink-0 rounded-xl border border-border bg-surface p-4 flex flex-col gap-3 h-fit md:sticky md:top-20">
<div className="flex flex-col gap-0.5">
<p className="text-[11px] font-semibold uppercase tracking-wider text-faint">참고</p>
<h2 className="text-sm font-semibold tracking-tight text-text-primary">
CTRS · 위기분류척도
</h2>
<p className="text-[11px] text-text-secondary leading-snug">
단계는 <span className="font-medium">위험성(Rating A)</span> 기준 자동 산출.
</p>
</div>

<ul className="flex flex-col gap-2">
{STAGES.map((s) => {
const { label, badge } = CTRS_META[s];
return (
<li key={s} className="flex gap-2.5">
<span
className={`mt-0.5 inline-flex h-fit items-center gap-1 px-2 py-0.5 rounded-full border text-[10px] font-semibold shrink-0 ${badge.bg} ${badge.border} ${badge.text}`}
>
<span className={`h-1.5 w-1.5 rounded-full ${badge.dot}`} aria-hidden />
{s} {label}
</span>
<span className="text-[11px] text-text-secondary leading-snug">{RATING_A[s]}</span>
</li>
);
})}
</ul>

<details className="border-t border-border pt-2.5">
<summary className="cursor-pointer text-[11px] font-medium text-text-secondary">
지지체계(Rating B) — 임상 참고 축
</summary>
<ul className="mt-2 flex flex-col gap-1.5">
{STAGES.map((s) => (
<li key={s} className="text-[11px] text-text-secondary leading-snug">
<span className="font-medium text-text-primary">{s}</span> · {RATING_B[s]}
</li>
))}
</ul>
</details>
</aside>
);
}
5 changes: 3 additions & 2 deletions apps/web/components/PatientListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Link from "next/link";

import type { PatientListItem as PatientListItemT } from "../lib/api";
import { RiskBadge } from "./RiskBadge";
import { CtrsBadge } from "./CtrsBadge";

function relative(iso: string | null): string {
if (!iso) return "—";
Expand Down Expand Up @@ -38,7 +38,8 @@ export function PatientListItem({ item }: { item: PatientListItemT }) {
</p>
</div>
<div className="flex flex-col items-end gap-1.5 shrink-0">
{item.latestRisk ? <RiskBadge level={item.latestRisk.level} /> : null}
{/* CTRS 위기분류척도 단계 — 전원 표기(위험신호 없으면 5·안정). */}
<CtrsBadge level={item.latestRisk?.level ?? null} />
{item.riskEventCount > 0 ? (
<span className="text-xs text-text-secondary tabular-nums">
위험 이벤트 {item.riskEventCount}건
Expand Down
41 changes: 41 additions & 0 deletions apps/web/lib/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,44 @@ export const riskBadge = {
} as const;

export type RiskLevel = keyof typeof riskColor;

/**
* CTRS(위기분류척도, Crisis Triage Rating Scale) 단계 표기.
* 대시보드 환자 목록은 safety risk level을 위험성(Rating A) 기준 CTRS 단계로 환산해
* 표기한다: critical→1(고위험) … RiskEvent 없음→5(안정). (앱 서버 `_RISK_TO_CTRS`와 동일)
*/
export type CtrsStage = 1 | 2 | 3 | 4 | 5;

export function ctrsFromLevel(level: RiskLevel | null | undefined): CtrsStage {
switch (level) {
case "critical":
return 1;
case "high":
return 2;
case "medium":
return 3;
case "low":
return 4;
default:
return 5; // RiskEvent 없음 = 위험 신호 없음
}
}

export const CTRS_META: Record<
CtrsStage,
{ label: string; badge: { bg: string; border: string; text: string; dot: string } }
> = {
1: { label: "고위험", badge: riskBadge.critical },
2: { label: "위험", badge: riskBadge.high },
3: { label: "주의", badge: riskBadge.medium },
4: { label: "관찰", badge: riskBadge.low },
5: {
label: "안정",
badge: {
bg: "bg-surface-elevated",
border: "border-border",
text: "text-text-secondary",
dot: "bg-faint",
},
},
};
Loading