헬스케어 CDSS 개발 패턴은(는) 무엇을 하나요?
EMR 워크플로에 통합되는 임상 의사결정 지원 시스템(CDSS)을 구축하기 위한 패턴입니다. CDSS 모듈은 환자 안전에 중요합니다 — 위음성(false negative)은 절대 허용되지 않습니다.
사용 시기
- 약물 상호작용 확인 구현
- 용량 검증 엔진 구축
- 임상 점수 시스템 구현 (NEWS2, qSOFA, APACHE, GCS)
- 비정상 임상 값에 대한 경고 시스템 설계
- 안전 점검이 포함된 약물 처방 입력 구축
- 임상 맥락을 고려한 검사 결과 해석 통합
작동 방식
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
↓ (사용자가 데이터 입력)
CDSS 엔진 (순수 함수, 부수 효과 없음)
├── 약물 상호작용 확인기
├── 용량 검증기
├── 임상 점수 (NEWS2, qSOFA 등)
└── 경고 분류기
↓ (경고 반환)
EMR UI (인라인으로 경고 표시, 임계 상태 시 차단)
약물 상호작용 확인
interface DrugInteractionPair {
drugA: string; // 일반명
drugB: string; // 일반명
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: `문서화된 알레르기와 교차 반응: ${allergy}`,
recommendation: '알레르기 상담 없이 처방하지 마십시오' });
}
}
return alerts.sort((a, b) => severityOrder(a.severity) - severityOrder(b.severity));
}
상호작용 쌍은 양방향이어야 합니다: 약물 A가 약물 B와 상호작용하면 약물 B도 약물 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: '사용 가능한 검증 규칙 없음', suggestedRange: null, factors: [] };
const factors: string[] = [];
// 안전: 규칙이 체중을 요구하지만 체중이 누락된 경우 차단 (통과하지 않음)
if (rules.weightBased) {
if (!patientWeight || patientWeight <= 0) {
return { valid: false, message: `${drug}에 체중 필요 (mg/kg 약물)`,
suggestedRange: null, factors: ['weight_missing'] };
}
factors.push('weight');
const maxDose = rules.maxPerKg * patientWeight;
if (dose > maxDose) {
return { valid: false, message: `${patientWeight}kg에 대한 최대 용량 초과`,
suggestedRange: { min: rules.minPerKg * patientWeight, max: maxDose, unit: rules.unit }, factors };
}
}
// 연령 기반 조정 (규칙이 연령 구간을 정의하고 환자 연령이 제공된 경우)
if (rules.ageAdjusted && patientAge !== undefined) {
factors.push('age');
const ageMax = rules.getAgeAdjustedMax(patientAge);
if (dose > ageMax) {
return { valid: false, message: `${patientAge}세에 대한 연령 조정 최대치 초과`,
suggestedRange: { min: rules.typicalMin, max: ageMax, unit: rules.unit }, factors };
}
}
// 신장 조정 (규칙이 eGFR 구간을 정의하고 eGFR이 제공된 경우)
if (rules.renalAdjusted && renalFunction !== undefined) {
factors.push('renal');
const renalMax = rules.getRenalAdjustedMax(renalFunction);
if (dose > renalMax) {
return { valid: false, message: `eGFR ${renalFunction}에 대한 신장 조정 최대치 초과`,
suggestedRange: { min: rules.typicalMin, max: renalMax, unit: rules.unit }, factors };
}
}
// 절대 최대치
if (dose > rules.absoluteMax) {
return { valid: false, message: `절대 최대치 ${rules.absoluteMax}${rules.unit} 초과`,
suggestedRange: { min: rules.typicalMin, max: rules.absoluteMax, unit: rules.unit },
factors: [...factors, 'absolute_max'] };
}
return { valid: true, message: '범위 내',
suggestedRange: { min: rules.typicalMin, max: rules.typicalMax, unit: rules.unit }, factors };
}
임상 점수: NEWS2
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 사양과 정확히 일치해야 합니다.
경고 심각도 및 UI 동작
| 심각도 | UI 동작 | 임상의 필요 조치 |
|---|---|---|
| 임계 | 작업 차단. 해제 불가능 모달. 빨간색. | 진행하려면 재정의 사유를 반드시 문서화해야 함 |
| 주요 | 인라인 경고 배너. 주황색. | 진행하기 전에 반드시 인지해야 함 |
| 경미 | 인라인 정보 메모. 노란색. | 인지만 필요, 조치 불필요 |
임계 경고는 절대 자동 해제되거나 토스트 알림으로 구현되어서는 안 됩니다. 재정의 사유는 감사 추적(audit trail)에 저장되어야 합니다.
CDSS 테스트 (위음성 허용 불가)
describe('CDSS — 환자 안전', () => {
INTERACTION_PAIRS.forEach(({ drugA, drugB, severity }) => {
it(`${drugA} + ${drugB} (${severity}) 감지`, () => {
const alerts = checkInteractions(drugA, [drugB], []);
expect(alerts.length).toBeGreaterThan(0);
expect(alerts[0].severity).toBe(severity);
});
it(`${drugB} + ${drugA} (역방향) 감지`, () => {
const alerts = checkInteractions(drugB, [drugA], []);
expect(alerts.length).toBeGreaterThan(0);
});
});
it('체중 누락 시 mg/kg 약물 차단', () => {
const result = validateDose('gentamicin', 300, 'iv');
expect(result.valid).toBe(false);
expect(result.factors).toContain('weight_missing');
});
it('잘못된 약물 데이터를 정상적으로 처리', () => {
expect(() => checkInteractions('', [], [])).not.toThrow();
});
});
통과 기준: 100%. 단 한 번의 누락된 상호작용도 환자 안전 사고입니다.
안티 패턴
- 문서화된 사유 없이 CDSS 검사를 선택 사항 또는 건너뛸 수 있도록 만들기
- 상호작용 검사를 토스트 알림으로 구현
- 약물 또는 임상 데이터에
any타입 사용 - 유지 관리 가능한 데이터 구조 대신 상호작용 쌍을 하드코딩
- CDSS 엔진에서 오류를 조용히 무시 (실패를 크게 알려야 함)
- 체중을 사용할 수 없을 때 체중 기반 검증 건너뛰기 (통과가 아닌 차단해야 함)
예시
예시 1: 약물 상호작용 확인
const alerts = checkInteractions('warfarin', ['aspirin', 'metformin'], ['penicillin']);
// [{ severity: 'critical', pair: ['warfarin', 'aspirin'],
// message: '출혈 위험 증가', recommendation: '병용 금지' }]
예시 2: 용량 검증
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: '절대 최대치 4000mg 초과' }
const noWeight = validateDose('gentamicin', 300, 'iv');
// { valid: false, factors: ['weight_missing'] }
예시 3: NEWS2 점수 계산
const result = calculateNEWS2({
respiratoryRate: 24, oxygenSaturation: 93, supplementalOxygen: true,
temperature: 38.5, systolicBP: 100, heartRate: 110, consciousness: 'voice'
});
// { total: 13, risk: 'high', escalation: '긴급 임상 검토 필요. ICU 고려.' }