From 5f895057100a14c9c07f28e060c98d4301de1864 Mon Sep 17 00:00:00 2001
From: Seohyunjho
Date: Wed, 29 Jul 2026 00:52:25 +0900
Subject: [PATCH 1/2] =?UTF-8?q?feat(web):=20=EB=8C=80=EC=8B=9C=EB=B3=B4?=
=?UTF-8?q?=EB=93=9C=20=ED=99=98=EC=9E=90=20=EB=AA=A9=EB=A1=9D=20=EC=9C=84?=
=?UTF-8?q?=ED=97=98=20=ED=91=9C=EA=B8=B0=EB=A5=BC=20CTRS=20=EB=8B=A8?=
=?UTF-8?q?=EA=B3=84=EB=A1=9C=20=EC=A0=84=ED=99=98=20+=20=EC=B2=99?=
=?UTF-8?q?=EB=8F=84=20=EB=B2=94=EB=A1=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
기존 safety risk level 라벨(안전/주의/위험/긴급)을 CTRS(위기분류척도) 단계로 표기.
위험성(Rating A) 기준으로 level→단계 환산(critical→1 고위험, high→2 위험,
medium→3 주의, low→4 관찰, RiskEvent 없음→5 안정), 전원 배지 표기(무위험=5·안정).
- lib/tokens: CtrsStage/ctrsFromLevel/CTRS_META 추가
- components/CtrsBadge: 목록 전용 `CTRS N · 라벨` 배지(항상 표기). 리포트/세션의
RiskBadge는 신호별 safety level 의미라 그대로 유지.
- components/CtrsLegend: 목록 우측 범례 — 위험성(Rating A) 5단계 정의 + 지지체계
(Rating B) 임상 참고 축(접이식). 사용자 제공 CTRS 정의 반영.
- dashboard/page: 목록+범례 2단 레이아웃(반응형).
Co-Authored-By: Claude Opus 4.8 (1M context)
---
apps/web/app/dashboard/page.tsx | 18 +++---
apps/web/components/CtrsBadge.tsx | 20 +++++++
apps/web/components/CtrsLegend.tsx | 73 +++++++++++++++++++++++++
apps/web/components/PatientListItem.tsx | 5 +-
apps/web/lib/tokens.ts | 41 ++++++++++++++
5 files changed, 148 insertions(+), 9 deletions(-)
create mode 100644 apps/web/components/CtrsBadge.tsx
create mode 100644 apps/web/components/CtrsLegend.tsx
diff --git a/apps/web/app/dashboard/page.tsx b/apps/web/app/dashboard/page.tsx
index 2275b4f..c07ea5c 100644
--- a/apps/web/app/dashboard/page.tsx
+++ b/apps/web/app/dashboard/page.tsx
@@ -1,3 +1,4 @@
+import { CtrsLegend } from "../../components/CtrsLegend";
import { PatientListItem } from "../../components/PatientListItem";
import { APIException, listPatients, type PatientListItem as Item } from "../../lib/api";
@@ -49,13 +50,16 @@ export default async function DashboardPage() {
{patients.length === 0 ? (
아직 등록된 환자가 없어요.
) : (
-
- {sorted.map((p) => (
- -
-
-
- ))}
-
+
+
+ {sorted.map((p) => (
+ -
+
+
+ ))}
+
+
+
)}
);
diff --git a/apps/web/components/CtrsBadge.tsx b/apps/web/components/CtrsBadge.tsx
new file mode 100644
index 0000000..c3f2493
--- /dev/null
+++ b/apps/web/components/CtrsBadge.tsx
@@ -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 (
+
+
+ CTRS {stage} · {label}
+
+ );
+}
diff --git a/apps/web/components/CtrsLegend.tsx b/apps/web/components/CtrsLegend.tsx
new file mode 100644
index 0000000..4562dca
--- /dev/null
+++ b/apps/web/components/CtrsLegend.tsx
@@ -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 = {
+ 1: "자살·자해 사고/관련 환청, 현병력 중 자살시도, 예측 불가한 폭력·충동",
+ 2: "자살 사고·행동으로 스스로 고통, 폭력·충동 과거력(현재 징후 없음)",
+ 3: "자·타해 사고를 양가적으로 표현하거나 비효과적 몸짓만 있음",
+ 4: "자·타해 사고/행동이 부분적·기왕력, 행동조절 욕구 분명·조절 가능",
+ 5: "자·타해 사고나 행동의 과거력·위험 없음",
+};
+
+// Rating B: 지지체계(Support System) — 참고 축.
+const RATING_B: Record = {
+ 1: "가족·친구 등 지지체계 전무, 기관의 즉각 지지도 불가",
+ 2: "동원 가능한 지지체계는 있으나 효과가 제한적",
+ 3: "잠재적 지지체계는 있으나 제대로 기능하기 어려움",
+ 4: "관심 있는 지지체계 있으나 지지 제공 능력·의지 다소 불명확",
+ 5: "관심 있는 지지체계가 지지 제공 능력·의지를 갖춤",
+};
+
+const STAGES: CtrsStage[] = [1, 2, 3, 4, 5];
+
+export function CtrsLegend() {
+ return (
+
+ );
+}
diff --git a/apps/web/components/PatientListItem.tsx b/apps/web/components/PatientListItem.tsx
index 4156203..2531c8b 100644
--- a/apps/web/components/PatientListItem.tsx
+++ b/apps/web/components/PatientListItem.tsx
@@ -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 "—";
@@ -38,7 +38,8 @@ export function PatientListItem({ item }: { item: PatientListItemT }) {
- {item.latestRisk ?
: null}
+ {/* CTRS 위기분류척도 단계 — 전원 표기(위험신호 없으면 5·안정). */}
+
{item.riskEventCount > 0 ? (
위험 이벤트 {item.riskEventCount}건
diff --git a/apps/web/lib/tokens.ts b/apps/web/lib/tokens.ts
index 7dc2fd9..59074e5 100644
--- a/apps/web/lib/tokens.ts
+++ b/apps/web/lib/tokens.ts
@@ -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",
+ },
+ },
+};
From 37511975d0b5b4290c6129c127ae7400ea92bd3c Mon Sep 17 00:00:00 2001
From: Seohyunjho
Date: Wed, 29 Jul 2026 01:00:51 +0900
Subject: [PATCH 2/2] =?UTF-8?q?feat(web):=20CTRS=20=EB=B2=94=EB=A1=80?=
=?UTF-8?q?=EB=A5=BC=20=EC=9A=B0=EC=B8=A1=20sticky=20=EC=BB=AC=EB=9F=BC?=
=?UTF-8?q?=EC=9C=BC=EB=A1=9C=20(md+=202=EB=8B=A8,=20=EC=8A=A4=ED=81=AC?=
=?UTF-8?q?=EB=A1=A4=20=EC=B6=94=EC=A0=81)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
lg→md 브레이크포인트로 낮춰 데스크톱 폭에서 하단 스택 방지, 범례 md:sticky top-20으로
스크롤 시 우측 상단 고정. 로컬 검증 완료.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
apps/web/app/dashboard/page.tsx | 2 +-
apps/web/components/CtrsLegend.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/web/app/dashboard/page.tsx b/apps/web/app/dashboard/page.tsx
index c07ea5c..c868212 100644
--- a/apps/web/app/dashboard/page.tsx
+++ b/apps/web/app/dashboard/page.tsx
@@ -50,7 +50,7 @@ export default async function DashboardPage() {
{patients.length === 0 ? (
아직 등록된 환자가 없어요.
) : (
-
+
{sorted.map((p) => (
-
diff --git a/apps/web/components/CtrsLegend.tsx b/apps/web/components/CtrsLegend.tsx
index 4562dca..2b6714e 100644
--- a/apps/web/components/CtrsLegend.tsx
+++ b/apps/web/components/CtrsLegend.tsx
@@ -28,7 +28,7 @@ const STAGES: CtrsStage[] = [1, 2, 3, 4, 5];
export function CtrsLegend() {
return (
-