의료 CDSS 개발 패턴은(는) 무엇을 하나요?
EMR 워크플로우에 통합되는 임상 의사결정 지원 시스템(CDSS) 구축을 위한 패턴입니다. CDSS 모듈은 환자 안전에 직결되므로 위음성(false negative)을 절대 허용하지 않습니다.
사용 시기
- 약물 상호작용 확인 구현
- 용량 검증 엔진 구축
- 임상 점수 시스템(NEWS2, qSOFA, APACHE, GCS) 구현
- 비정상 임상 값에 대한 알림 시스템 설계
- 안전 점검을 포함한 약물 처방 입력 구축
- 임상 맥락을 고려한 검사 결과 해석 시스템 구축
작동 방식
CDSS 엔진은 **부작용이 없는 순수 함수 라이브러리(pure function library with zero side effects)**입니다. 임상 데이터를 입력하면 알림을 출력합니다. 이로 인해 완전히 테스트 가능합니다.
세 가지 주요 모듈:
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));
}
상호작용 쌍은 **양방향(bidirectional)**이어야 합니다. 약물 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 동작 | 임상의 조치 필요 사항 |
|---|---|---|
| 위험(Critical) | 작업 차단. 해제 불가능 모달. 빨간색. | 진행하려면 재정의 사유를 반드시 문서로 기록해야 함 |
| 주요(Major) | 인라인 경고 배너. 주황색. | 진행하기 전에 반드시 승인해야 함 |
| 경미(Minor) | 인라인 정보 표시. 노란색. | 인식만, 조치 불필요 |
위험 알림(Critical)은 절대 자동 해제되거나 토스트 알림으로 구현되어서는 안 됩니다. 재정의 사유는 감사 추적(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 고려.' }