From ecc
임상 의사결정지원시스템(CDSS) 개발 패턴입니다. 약물 상호작용 검사, 용량 검증, 임상 점수(NEWS2, qSOFA), 경고 심각도 분류, EMR 워크플로 통합을 다룹니다.
npx claudepluginhub sam42-lab/everything-claude-code-krThis skill uses the workspace's default tool permissions.
EMR 워크플로에 통합되는 임상 의사결정지원시스템을 만들기 위한 패턴입니다. CDSS 모듈은 환자 안전 핵심 영역이므로 false negative에 대한 허용치는 0입니다.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
EMR 워크플로에 통합되는 임상 의사결정지원시스템을 만들기 위한 패턴입니다. CDSS 모듈은 환자 안전 핵심 영역이므로 false negative에 대한 허용치는 0입니다.
CDSS 엔진은 부작용이 전혀 없는 순수 함수 라이브러리입니다. 임상 데이터를 입력하면 경고를 출력합니다. 이 구조 덕분에 완전한 테스트가 가능합니다.
주요 모듈 세 가지:
checkInteractions(newDrug, currentMeds, allergies) — 신규 약물을 현재 약물 및 알레르기와 비교합니다. 심각도 순으로 정렬된 InteractionAlert[]를 반환합니다. DrugInteractionPair 데이터 모델을 사용합니다.validateDose(drug, dose, route, weight, age, renalFunction) — 처방 용량이 체중 기반, 연령 보정, 신기능 보정 규칙을 만족하는지 검증합니다. DoseValidationResult를 반환합니다.calculateNEWS2(vitals) — NEWS2Input으로부터 National Early Warning Score 2를 계산합니다. 총점, 위험 수준, 에스컬레이션 지침이 담긴 NEWS2Result를 반환합니다.EMR UI
↓ (user enters data)
CDSS Engine (pure functions, no side effects)
├── Drug Interaction Checker
├── Dose Validator
├── Clinical Scoring (NEWS2, qSOFA, etc.)
└── Alert Classifier
↓ (returns alerts)
EMR UI (displays alerts inline, blocks if critical)
interface DrugInteractionPair {
drugA: string; // generic name
drugB: string; // generic name
severity: 'critical' | 'major' | 'minor';
mechanism: string;
clinicalEffect: string;
recommendation: string;
}
function checkInteractions(
newDrug: string,
currentMedications: string[],
allergyList: string[]
): InteractionAlert[] {
if (!newDrug) return [];
const alerts: InteractionAlert[] = [];
for (const current of currentMedications) {
const interaction = findInteraction(newDrug, current);
if (interaction) {
alerts.push({ severity: interaction.severity, pair: [newDrug, current],
message: interaction.clinicalEffect, recommendation: interaction.recommendation });
}
}
for (const allergy of allergyList) {
if (isCrossReactive(newDrug, allergy)) {
alerts.push({ severity: 'critical', pair: [newDrug, allergy],
message: `Cross-reactivity with documented allergy: ${allergy}`,
recommendation: 'Do not prescribe without allergy consultation' });
}
}
return alerts.sort((a, b) => severityOrder(a.severity) - severityOrder(b.severity));
}
상호작용 쌍은 반드시 양방향이어야 합니다. Drug A가 Drug B와 상호작용한다면, Drug B도 Drug A와 상호작용해야 합니다.
interface DoseValidationResult {
valid: boolean;
message: string;
suggestedRange: { min: number; max: number; unit: string } | null;
factors: string[];
}
function validateDose(
drug: string,
dose: number,
route: 'oral' | 'iv' | 'im' | 'sc' | 'topical',
patientWeight?: number,
patientAge?: number,
renalFunction?: number
): DoseValidationResult {
const rules = getDoseRules(drug, route);
if (!rules) return { valid: true, message: 'No validation rules available', suggestedRange: null, factors: [] };
const factors: string[] = [];
// SAFETY: if rules require weight but weight missing, BLOCK (not pass)
if (rules.weightBased) {
if (!patientWeight || patientWeight <= 0) {
return { valid: false, message: `Weight required for ${drug} (mg/kg drug)`,
suggestedRange: null, factors: ['weight_missing'] };
}
factors.push('weight');
const maxDose = rules.maxPerKg * patientWeight;
if (dose > maxDose) {
return { valid: false, message: `Dose exceeds max for ${patientWeight}kg`,
suggestedRange: { min: rules.minPerKg * patientWeight, max: maxDose, unit: rules.unit }, factors };
}
}
// Age-based adjustment (when rules define age brackets and age is provided)
if (rules.ageAdjusted && patientAge !== undefined) {
factors.push('age');
const ageMax = rules.getAgeAdjustedMax(patientAge);
if (dose > ageMax) {
return { valid: false, message: `Exceeds age-adjusted max for ${patientAge}yr`,
suggestedRange: { min: rules.typicalMin, max: ageMax, unit: rules.unit }, factors };
}
}
// Renal adjustment (when rules define eGFR brackets and eGFR is provided)
if (rules.renalAdjusted && renalFunction !== undefined) {
factors.push('renal');
const renalMax = rules.getRenalAdjustedMax(renalFunction);
if (dose > renalMax) {
return { valid: false, message: `Exceeds renal-adjusted max for eGFR ${renalFunction}`,
suggestedRange: { min: rules.typicalMin, max: renalMax, unit: rules.unit }, factors };
}
}
// Absolute max
if (dose > rules.absoluteMax) {
return { valid: false, message: `Exceeds absolute max ${rules.absoluteMax}${rules.unit}`,
suggestedRange: { min: rules.typicalMin, max: rules.absoluteMax, unit: rules.unit },
factors: [...factors, 'absolute_max'] };
}
return { valid: true, message: 'Within range',
suggestedRange: { min: rules.typicalMin, max: rules.typicalMax, unit: rules.unit }, factors };
}
interface NEWS2Input {
respiratoryRate: number; oxygenSaturation: number; supplementalOxygen: boolean;
temperature: number; systolicBP: number; heartRate: number;
consciousness: 'alert' | 'voice' | 'pain' | 'unresponsive';
}
interface NEWS2Result {
total: number; // 0-20
risk: 'low' | 'low-medium' | 'medium' | 'high';
components: Record<string, number>;
escalation: string;
}
점수표는 Royal College of Physicians 명세와 정확히 일치해야 합니다.
| Severity | UI 동작 | 임상의 조치 필요 여부 |
|---|---|---|
| Critical | 작업 차단. 닫을 수 없는 모달. 빨강. | 진행하려면 override 이유를 반드시 기록 |
| Major | 인라인 경고 배너. 주황. | 진행 전 acknowledge 필요 |
| Minor | 인라인 정보 노트. 노랑. | 참고용, 별도 조치 불필요 |
Critical 경고는 절대 자동 dismiss되면 안 되고, toast로 구현해서도 안 됩니다. Override 사유는 반드시 audit trail에 저장해야 합니다.
describe('CDSS — Patient Safety', () => {
INTERACTION_PAIRS.forEach(({ drugA, drugB, severity }) => {
it(`detects ${drugA} + ${drugB} (${severity})`, () => {
const alerts = checkInteractions(drugA, [drugB], []);
expect(alerts.length).toBeGreaterThan(0);
expect(alerts[0].severity).toBe(severity);
});
it(`detects ${drugB} + ${drugA} (reverse)`, () => {
const alerts = checkInteractions(drugB, [drugA], []);
expect(alerts.length).toBeGreaterThan(0);
});
});
it('blocks mg/kg drug when weight is missing', () => {
const result = validateDose('gentamicin', 300, 'iv');
expect(result.valid).toBe(false);
expect(result.factors).toContain('weight_missing');
});
it('handles malformed drug data gracefully', () => {
expect(() => checkInteractions('', [], [])).not.toThrow();
});
});
통과 기준은 100%입니다. 상호작용 하나라도 놓치면 환자 안전 사고입니다.
any 타입을 사용하는 것const alerts = checkInteractions('warfarin', ['aspirin', 'metformin'], ['penicillin']);
// [{ severity: 'critical', pair: ['warfarin', 'aspirin'],
// message: 'Increased bleeding risk', recommendation: 'Avoid combination' }]
const ok = validateDose('paracetamol', 1000, 'oral', 70, 45);
// { valid: true, suggestedRange: { min: 500, max: 4000, unit: 'mg' } }
const bad = validateDose('paracetamol', 5000, 'oral', 70, 45);
// { valid: false, message: 'Exceeds absolute max 4000mg' }
const noWeight = validateDose('gentamicin', 300, 'iv');
// { valid: false, factors: ['weight_missing'] }
const result = calculateNEWS2({
respiratoryRate: 24, oxygenSaturation: 93, supplementalOxygen: true,
temperature: 38.5, systolicBP: 100, heartRate: 110, consciousness: 'voice'
});
// { total: 13, risk: 'high', escalation: 'Urgent clinical review. Consider ICU.' }