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) => ( +
  • + +
  • + ))} +
+ +
)} ); 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 ( -