제4장

제4장: 데이터 형식 II — 안전성 데이터: 이상사례·MedDRA·CTCAE

WIA-BIO-010이 이상사례를 어떻게 기록하는지, 그 기록을 어떻게 코딩·등급화·평가하는지, 어떤 규제 시한이 시작되는지, 그리고 참조 SDK와 CLI가 어디서 규칙을 잘못 적용하는지를 다룬다.

유효성 데이터는 시험이 묻고자 설계된 질문에 답한다. 안전성 데이터는 원하든 원치 않든 모든 임상시험이 답해야 하는 질문, 즉 참여한 사람들에게 무슨 일이 일어났는가에 답한다. 그래서 안전성 데이터 모델은 제3장에서 본 모델과 성격이 다르다. 방문과 검사 패널은 미리 계획되어 일정대로 들어오지만, 이상사례는 발생하는 순간에 들어오고, 보고자의 표현 그대로 기술되며, 추적 정보가 들어올 때마다 바뀌고, 경우에 따라서는 의뢰자가 며칠 안에 규제기관에 보고해야 하는 의무를 만든다. 안전성 기록은 의학 코더, 안전성 담당 의사, 통계 담당자, 규제기관이 같은 기록에서 같은 결론에 이를 수 있을 만큼의 구조를 갖춰야 한다.

WIA-BIO-010 v1.0은 이 영역을 §5(MedDRA 코딩, 중증도 등급, 인과관계, 중대한 이상사례, 안전성 모니터링)와 §7.3의 이상사례 JSON 객체에서 다룬다. 참조 패키지 @wia/bio-010은 같은 개념을 AdverseEvent, MedDRACode, SeriousnessCriteria, AESeverity, Causality, AEOutcome 타입과 recordAdverseEvent 메서드로 구현한다. 이 장은 세 출처를 나란히 놓고 읽는다. ICH·FDA 정의와 일치하는 부분은 왜 그런 구조인지를 설명하고, 일치하지 않는 부분은 그렇다고 밝히고 수정안을 제시한다. 잘못된 보고 기한을 계산하는 안전성 데이터베이스는 기한을 아예 계산하지 않는 것보다 더 위험하기 때문이다.

이 장 전체를 관통하는 질문은 세 가지이며, 안전성 데이터 오류의 대부분은 그중 두 가지를 섞는 데서 생긴다. 얼마나 심했는가(중증도, CTCAE로 등급화), 어떤 결과로 이어졌는가(중대성, 규제상의 분류), 시험약이 원인이었는가(인과관계, 시험자와 의뢰자가 평가)이다. 네 번째 질문인 예상된 것이었는가(예상성)는 v1.0에 전혀 모델링되어 있지 않지만, 신속보고 대상 여부를 결정하는 것은 바로 이 질문이다.

4.1 AE·SAE·SUSAR — 중증도는 중대성이 아니다

이후 논의가 기대는 정의

ICH E2A는 이상사례(AE)를 의약품을 투여받은 환자나 임상시험 피험자에게 발생한 바람직하지 않은 모든 의학적 사건으로 정의하며, 해당 약물과 반드시 인과관계가 있어야 하는 것은 아니라고 명시한다. 정의는 의도적으로 넓다. 시험 기간 중 빙판길에서 넘어진 것도 이상사례다. 추정 원인으로 먼저 걸러내지 않고 모든 사건을 기록해야 나중에 예상하지 못한 신호를 찾아낼 수 있다.

약물이상반응은 그중 약물과의 인과관계가 적어도 합리적으로 가능한 사건으로 범위를 좁힌 것이다. 미국 규정에서는 이에 해당하는 표현으로 suspected adverse reaction(의심되는 이상반응)을 쓴다(21 CFR 312.32(a)).

중대한 이상사례(SAE)는 사망, 생명을 위협하는 상황, 입원 등 4.2절에 정리한 결과 기반 기준 가운데 하나 이상을 충족하는 이상사례다. 중대성은 사건의 결과에 붙는 속성으로 규정이 정하며, 보고 의무를 발생시키는 것이 바로 이 중대성이다.

예상하지 못한 중대한 약물이상반응(SUSAR)은 세 조건의 교집합이다. 사건이 중대하고, 인과관계가 의심되며, 사건의 성격이나 정도가 참고 안전성 정보(대개 시험자 자료집)와 일치하지 않아야 한다. SUSAR라는 용어는 유럽에서 온 것이고, 미국 규정은 같은 개념을 "중대하고 예상하지 못한 의심 이상반응"으로 표현한다. 4.6절에서 다룰 7일·15일 신속보고 시한이 걸리는 것은 모든 SAE가 아니라 이 교집합이다.

중증도와 중대성은 서로 독립된 축이다

일상 언어에서 "심하다"와 "중대하다"는 거의 같은 말이다. 임상 안전성 데이터에서는 그렇지 않다. 중증도는 강도, 즉 사건이 피험자에게 얼마나 강하게 영향을 주었는지를 뜻하며, 경증/중등증/중증 또는 CTCAE 1–5등급 같은 척도로 기록한다. 중대성은 사건이 규제상의 결과 기준을 충족했는지를 뜻한다. 피험자가 하루 동안 누워 있어야 했던 심한 두통은 중증이지만, 다른 일이 없었다면 중대하지 않다. 관찰을 위해 하룻밤 입원한 가벼운 구토는 중증이 아니지만, 입원이라는 결과 때문에 중대하다. 아래 표의 네 사례는 가능한 조합을 모두 보여 주도록 고른 교육용 예시이며, 특정 약물에 대한 규칙이 아니다.

사례(예시)중증도 / CTCAE중대한가이유
하루 동안 누워 지내야 했던 두통, 집에서 경구 진통제로 치료중증(자기관리 활동을 제한하면 3등급)아니오강도는 높지만 결과 기준을 하나도 충족하지 않는다.
가벼운 구토, 관찰을 위해 하룻밤 입원경증(1등급)예입원은 강도와 관계없이 중대성 기준이다.
정기 채혈에서 발견된 4등급 검사 이상, 피험자는 무증상이고 외래에서 관리4등급사례별 판단CTCAE 4등급이라는 사실만으로 규제상의 "생명을 위협하는"(사건 당시 즉각적인 사망 위험)이 성립하지는 않는다. 이런 경우의 처리 방식은 계획서나 안전성 관리 계획에 정해 두어야 한다.
응급실에서 집중 치료를 받고 당일 퇴원한 급성 알레르기 반응시험자 판단에 따라 중증 또는 생명 위협예입원은 없었지만, FDA는 응급실에서 집중 치료가 필요했던 알레르기성 기관지경련을 중요한 의학적 사건의 예로 든다.

"얼마나 나빴는가"를 필드 하나로만 담는 데이터 모델은 이 네 사례를 올바르게 표현할 수 없다. WIA-BIO-010은 중증도(severity)와 중대성(seriousness)을 별도 속성으로 둔다. 출발점으로는 옳다. 문제는 다음 절부터 보듯 각 속성의 내부에서 시작된다.

4.2 중대성 기준과 SeriousnessCriteria 객체

명세 §5.4는 21 CFR 312.32를 인용하며 여섯 가지 기준을 나열한다. SDK는 이를 불리언 플래그 여섯 개와 요약 플래그 serious로 표현한다. 아래 표는 규정 문구, 명세 문구, SDK 필드, 그리고 SDTM AE 도메인의 대응 변수를 나란히 놓은 것이다.

기준(21 CFR 312.32 / ICH E2A)명세 §5.4 문구SDK 필드SDTM AE 변수
사망에 이른 경우Results in deathdeathAESDTH
생명을 위협하는 경우(사건 당시 즉각적인 사망 위험)Is life-threateninglifeThreateningAESLIFE
입원 또는 입원 기간의 연장Requires hospitalization or prolongationhospitalizationAESHOSP
지속적이거나 중대한 장애·기능 저하, 또는 정상적인 일상생활 능력의 상당한 손상Persistent or significant disability/incapacitydisabilityAESDISAB
선천적 기형 또는 이상Congenital anomaly/birth defectcongenitalAnomalyAESCONG
피험자를 위태롭게 할 수 있고 위 결과를 막기 위해 처치가 필요할 수 있는 중요한 의학적 사건"Requires intervention to prevent permanent impairment or damage"otherAESMIE
(요약)—seriousAESER(Y/N)

해석상 짚어 둘 세 가지

"생명을 위협하는"은 사건 당시를 기준으로 판단한다. 미국 규정의 정의는 시험자나 의뢰자가 보기에 피험자를 즉각적인 사망 위험에 놓이게 한 사건을 가리킨다. 더 심한 형태로 일어났다면 사망을 초래했을 수도 있는 사건은 포함하지 않는다. 4등급 검사 수치가 자동으로 "생명을 위협하는" SAE가 되지 않는 이유가 여기에 있다.

명세의 여섯 번째 기준은 문구가 틀렸다. "영구적 손상을 막기 위한 처치가 필요한 경우"는 의료기기 보고 규정의 표현이지 21 CFR 312.32의 표현이 아니다. 의약품 규정의 여섯 번째 기준은 중요한 의학적 사건이다. 다른 기준에는 해당하지 않지만, 적절한 의학적 판단에 따라 피험자를 위태롭게 할 수 있고 위에 열거한 결과를 막기 위해 처치가 필요할 수 있는 사건을 말한다. SDK의 필드 이름 other와 주석 "Other medically important condition"은 올바른 개념과 일치한다. 구현자는 명세 문구가 아니라 SDK 주석과 규정을 따라야 한다.

입원 기준에는 계획서 수준의 규칙이 필요하다. 시험 참여 전에 이미 예정되어 있던 입원, 사회적 사유의 입원, 악화되지 않은 기존 질환에 대한 선택적 시술을 위한 입원은 흔히 입원 기준에서 제외된다. 다만 계획서나 안전성 관리 계획에 그렇게 정해 두었을 때만 그렇다. 데이터 모델은 분류를 나중에 검토할 수 있도록 입원일·퇴원일 같은 원자료를 함께 보존해야 한다.

요약 플래그는 입력받지 말고 도출해야 한다

SeriousnessCriteria에서 serious는 독립된 불리언이다. hospitalization: true이면서 serious: false인 기록을 막는 장치가 없고, SDK의 recordAdverseEvent는 보고 필요 여부를 serious 하나만 보고 결정한다. 따라서 그런 기록은 아무 경고 없이 보고 대상이 아닌 것으로 처리된다. 구현이 지켜야 할 규칙은 단순하다. 여섯 기준 중 하나라도 참이면 serious는 참이고, 그 역도 성립해야 한다. 값을 계산해서 채우거나, 기준과 어긋나는 기록을 거부한다(SDK에는 바로 이런 경우를 위한 ClinicalTrialErrorCode.VALIDATION_FAILURE, 즉 B003이 정의되어 있다). 일관성 검사기는 4.7절에 제시한다.

4.3 MedDRA — 원문 용어에서 기관계 대분류까지

시험자는 사건을 자기 말로 기술한다. "혈당 떨어짐", "저혈당 온 듯", "손 떨림, 혈당 52" 같은 식이다. 이런 기술을 세고, 기관 간에 비교하고, 규제 제출용 표로 요약하려면 먼저 통제 어휘로 옮겨야 한다. 임상시험 이상사례에서 그 어휘는 ICH 체계에서 관리되고 유지관리 기관이 구독자에게 배포하는 MedDRA(Medical Dictionary for Regulatory Activities)다.

다섯 단계

MedDRA는 다섯 단계의 계층 구조다. 명세 §5.1은 심방세동을 예로 들고 있으며, 그림도 같은 연결을 보여 준다.

SOC — 기관계 대분류 HLGT — 상위 그룹 용어 HLT — 상위 용어 PT — 기본 단어 LLT — 최하위 용어 Cardiac disorders Cardiac arrhythmias Supraventricular arrhythmias Atrial fibrillation (분석 단위) 원문과 가장 가까운 용어 (코딩 단위)
그림 4.1 명세 §5.1 예시로 본 MedDRA 계층. 코더는 LLT를 선택하고, 요약표는 보통 SOC 안의 PT 단위로 집계한다.

계층의 양 끝은 하는 일이 다르다. LLT는 진입점이다. 동의어, 어휘 변형, 철자 변형을 담고 있어 원문 용어를 대개 문자 그대로 또는 거의 그대로 대응시킬 수 있다. 각 LLT는 정확히 하나의 PT에 연결되고, PT는 하나의 의학적 개념을 나타내며 사건을 집계하는 기본 단위가 된다. PT는 HLT와 HLGT를 거쳐 SOC로 묶인다. MedDRA는 다축(multiaxial) 구조여서 하나의 PT가 여러 SOC 아래에 나타날 수 있지만, 각 PT에는 지정된 일차 SOC가 하나 있고, 표준 요약표와 SDTM의 AEBODSYS에 들어가는 것은 이 일차 SOC다.

코딩 실무

  1. 원문 용어를 보존한다. 원문은 원자료다. WIA JSON에서는 term, SDTM에서는 AETERM이다. 코딩은 필드를 덧붙이는 작업이지 시험자의 표현을 덮어쓰는 작업이 아니다.
  2. 가장 가까운 LLT로 코딩한 뒤 그 PT를 따른다. "그럴듯해 보이는" PT를 고르지 않는다. LLT → PT 연결은 사전이 정한다.
  3. 추측하지 말고 질의한다. 두 개념이 섞인 원문("오심 및 구토")이나 모호한 원문("반응")은 데이터 질의로 기관에 되돌려야 한다. 추측한 개념 하나로 코딩하면 데이터 품질 문제가 가려진다.
  4. 사전 버전을 기록한다. MedDRA는 1년에 두 번 갱신된다. 용어가 추가되고, 드물게 LLT가 비현행(non-current)으로 바뀌거나 PT의 위치가 옮겨진다. 명세의 참고문헌은 MedDRA 26.0을 인용한다. 운영 시스템은 각 코딩 기록이 어느 버전을 썼는지 남기고, 시험이 버전을 올릴 때 재코딩하고 그 결과 달라진 부분을 검토해야 한다.

MedDRACode 타입

SDK는 코딩 결과를 다음과 같이 표현한다(주석 축약).

export interface MedDRACode {
  pt: string;       // Preferred Term code
  ptTerm: string;   // Preferred Term text
  soc: string;      // System Organ Class code
  socTerm: string;  // System Organ Class text
  hlt?: string;     // High Level Term
  hlgt?: string;    // High Level Group Term
}

§7.3 예시는 이 타입을 PT 10020993 "Hypoglycaemia"와 SOC 10027433 "Metabolism and nutrition disorders"로 채운다. PT 텍스트가 영국식 철자라는 점에 주목하자. MedDRA는 PT에 영국식 영어를 쓰고, "Hypoglycemia" 같은 미국식 철자는 대개 그 PT를 가리키는 LLT로 둔다. 같은 예시의 원문 "Hypoglycemia"는 오타가 아니며, LLT를 거치지 않고 문자열만 비교하는 매칭 알고리즘은 이 둘을 놓친다.

타입에는 두 가지가 빠져 있다. 실제로 코딩 판단이 이루어지고 감사되는 단위가 LLT인데도 LLT 필드가 없다(SDTM에 AELLT, AELLTCD가 있는 이유다). 사전 버전 필드도 없어서, 한 릴리스로 코딩된 기록과 다른 릴리스로 코딩된 기록을 구별할 수 없다. 선택 필드 hlt, hlgt는 pt, soc와 달리 코드와 텍스트가 분리되지 않은 단일 문자열이다. 하위 호환을 유지하는 확장으로 기존 필드는 그대로 두고 llt, lltTerm, meddraVersion을 추가할 수 있다. 4.7절의 확장 기록이 그 예다.

4.4 CTCAE 등급과 AESeverity의 문제

강도에 대해서 명세 §5.2는 미국 국립암연구소(NCI)가 발간하는 CTCAE(Common Terminology Criteria for Adverse Events)를 채택하며, 참고문헌에서 5.0 버전을 인용한다. CTCAE는 종양 임상시험을 위해 만들어졌지만, "중등증"과 "중증"의 구분을 개인 판단에 맡기지 않고 용어별로 등급 정의를 제공하기 때문에(예를 들어 설사 2등급과 3등급을 무엇으로 가르는지) 훨씬 넓게 쓰인다. 일반 등급 정의는 다음과 같다.

등급일반 정의(명세 §5.2, CTCAE)SDK ctcaeGrade가장 가까운 SDK AESeverity 값
1경증. 무증상 또는 가벼운 증상, 임상적·진단적 관찰만 해당, 처치 불필요1mild
2중등증. 최소한의 국소적·비침습적 처치 필요, 연령에 맞는 도구적 일상생활 활동 제한2moderate
3중증 또는 의학적으로 유의하나 즉각적으로 생명을 위협하지는 않음, 입원 또는 입원 연장이 필요, 장애 유발, 자기관리 일상생활 활동 제한3severe
4생명을 위협하는 결과, 긴급 처치 필요4life-threatening
5이상사례와 관련된 사망5death

정의 자체에서 두 가지 주의점이 나온다. 첫째, 모든 CTCAE 용어가 모든 등급을 쓰지는 않는다. 3등급에서 끝나는 용어도 있으며, 용어별 기술이 언제나 일반 정의보다 우선한다. 둘째, 3등급 정의의 "입원이 필요"는 중대성 기준인 입원이 실제로 일어난 것과 겹치지만 같지 않다. 등급과 중대성은 여전히 따로 기록해야 한다.

SDK가 두 척도를 섞는 곳

AdverseEvent 타입에는 강도 필드가 두 개 있다. 필수 필드 severity: AESeverity와 선택 필드 ctcaeGrade?: 1 | 2 | 3 | 4 | 5다. 유니언 AESeverity = 'mild' | 'moderate' | 'severe' | 'life-threatening' | 'death'는 서로 다른 두 개념을 섞고 있다. 앞의 세 값은 종양 외 영역의 많은 계획서가 쓰는 전통적인 3점 강도 척도이고, 뒤의 두 값은 CTCAE 4·5등급의 서술어다. 게다가 이 두 값은 다른 곳에 있는 정보와 중복된다. "사망"은 중증도 값, seriousness.death, outcome: 'fatal'의 세 곳에 나타나고, "생명 위협"은 중증도 값과 seriousness.lifeThreatening 두 곳에 나타난다.

결과는 실무적인 문제로 이어진다. 3점 척도를 쓰는 계획서에서는 가벼운 사건이 사망으로 끝난 경우를 표현할 유효한 값이 없다. severity를 SDTM AESEV(통제 용어 MILD, MODERATE, SEVERE)로 옮기는 후속 시스템은 다섯 값 중 두 값의 대상이 없다. 그리고 사망에 대해 일치해야 할 세 필드가 서로 달라도 아무 오류가 나지 않는다.

구현자에게 권하는 방식은 두 척도를 분리하고, 어느 쪽을 주 척도로 할지 계획서가 정하게 하는 것이다.

4.5 인과관계 — WHO-UMC 범주와 보고 판단

임상시험의 모든 이상사례에는 시험자의 인과관계 평가가 붙는다. 시험약이 이 사건을 일으켰을 가능성이 얼마나 되는가라는 질문이다. 명세 §5.3은 WHO 웁살라 모니터링 센터(WHO-UMC) 체계의 범주를 채택하고, SDK는 이를 Causality = 'certain' | 'probable' | 'possible' | 'unlikely' | 'unassessable'로 표현한다.

범주핵심 특징(WHO-UMC 요약)보고용 이분화 관행
certain(확실함)시간적 관계가 타당함, 질병이나 다른 약물로 설명되지 않음, 투약 중단 후 반응이 타당함, 필요하면 재투여 결과도 부합함관련 있음
probable(상당히 확실함)시간적 관계가 합리적임, 질병이나 다른 약물 때문일 가능성이 낮음, 중단 후 반응이 임상적으로 합리적임, 재투여 불필요관련 있음
possible(가능함)시간적 관계는 합리적이나 질병이나 다른 약물로도 설명될 수 있음, 중단 후 정보가 부족할 수 있음관련 있음
unlikely(가능성 적음)시간적 관계가 불가능하지는 않으나 개연성이 낮음, 질병이나 다른 약물이 그럴듯한 설명을 제공함관련 없음
unassessable(평가 불가)정보가 불충분하거나 모순되며 보완·확인이 불가능함명확해질 때까지 관련 있음으로 취급(보수적 관행)

WHO-UMC 체계에는 추가 자료를 확보하는 중인 사례를 위한 조건부/미분류 범주도 있다. 명세와 SDK는 이를 생략했다. 실무에서는 이런 사례를 질의를 열어 둔 채 unassessable로 두는 경우가 많으며, 감사추적에서 이후 재평가가 확인된다면 받아들일 만하다.

이분화가 필요한 이유

규정은 WHO-UMC 척도를 쓰지 않는다. 미국 규정은 약물이 사건을 일으켰을 "합리적 가능성"이 있는지를 기준으로 삼고, ICH E2A도 비슷한 표현을 쓴다. 따라서 안전성 시스템에는 시험자의 범주를 관련 있음/관련 없음의 이진 판단으로 옮기는 문서화된 대응 규칙이 필요하다. 표의 오른쪽 열은 널리 쓰이는 관행일 뿐 규제 요건이 아니다. 이 대응 규칙은 모든 사례에 똑같이 적용되도록 안전성 관리 계획에 명시해야 한다.

데이터 설계 측면에서 두 가지가 더 중요하다. 시험자의 평가와 의뢰자의 평가는 다를 수 있고, 신속보고에서 의뢰자는 근거 없이 시험자의 관련성 판단을 낮출 수 없다. causality 필드 하나로는 두 평가를 모두 담을 수 없다. 또 시험약과 배경 요법처럼 제품이 둘 이상인 시험은 제품별 평가가 필요하다. 실제 약물감시를 지원하려는 구현이라면 이 두 이유로 평가자·제품·일자를 담은 평가 배열로 모델을 확장해야 한다.

SDK의 recordAdverseEvent는 보고 필요 여부를 정할 때 causality를 전혀 읽지 않는다. 이것이 다음 절의 주제다.

4.6 신속보고 시한

서로 다른 두 의무

보고 규칙은 흔히 "SAE는 7일 또는 15일 안에 보고"로 요약된다. 이 요약은 당사자, 발동 조건, 기한이 서로 다른 두 의무를 하나로 뭉친 것이다.

의무보고 방향대상기한
SAE 통보시험자 → 의뢰자관련성 판단과 무관하게 모든 SAE(21 CFR 312.64(b), ICH E6)즉시. 실무 기한은 계획서가 정한다
신속 안전성 보고 — 사망 또는 생명 위협의뢰자 → 규제기관(및 시험자)결과가 사망이거나 생명을 위협한, 의심되고 예상하지 못한 중대한 반응가능한 한 빨리, 의뢰자 인지 후 7 역일 이내
7일 보고의 추가 보고의뢰자 → 규제기관같은 사례추가 8 역일 이내에 가능한 한 완전한 보고(ICH E2A)
신속 안전성 보고 — 그 밖의 경우의뢰자 → 규제기관(및 시험자)그 밖의 모든 의심되고 예상하지 못한 중대한 반응의뢰자 인지 후 15 역일 이내

규제기관 대상 기한은 ICH E2A에서 나왔고, 미국에서는 21 CFR 312.32(c), 유럽에서는 SUSAR에 관한 EU 임상시험규정(Regulation (EU) No 536/2014)으로 구현되어 있다. 국내에서도 식품의약품안전처가 의약품 임상시험 관리기준에 따라 SUSAR에 이와 비슷한 7일·15일 기한을 적용한다. 국내에 제출하는 독자는 정확한 문구를 최신 식약처 고시로 확인해야 한다. 명세 §5.4와 SDK 상수(FATAL_SAE_REPORTING_DAYS: 7, SAE_REPORTING_DAYS: 15)는 숫자는 정확히 옮겼지만, 그 숫자를 엉뚱한 사건 집합에 붙였다.

0일은 발생일이 아니라 의뢰자 인지일이다

시한은 의뢰자(또는 CRO처럼 의뢰자를 대신하는 자)가 유효한 사례의 최소 요건, 즉 식별 가능한 피험자, 식별 가능한 보고자, 의심 제품, 사건을 갖춘 정보를 처음 받은 날부터 시작한다. 그날은 발생일로부터 며칠, 길게는 몇 달 뒤일 수 있다. 방문 사이에 시작된 사건은 다음 방문에서야 드러날 수 있기 때문이다. 시한은 인지일로부터만 계산할 수 있으므로, 의뢰자가 정보를 받은 날짜를 데이터로 기록해야 한다. 명세의 AE 객체에도, SDK 타입에도 그런 필드가 없다.

발생 03-10 기관 인지 03-11 0일 의뢰자 인지 03-12 7일 사망·생명 위협 03-19 15일 그 밖의 SUSAR / 7+8 추가 보고 03-27 SDK: startDate + 15일 → 03-25 (발생일 기준, 잘못된 기준일)
그림 4.2 2025년의 예시 사례로 본 신속보고 시한. 기한은 의뢰자 인지일로부터 역일로 센다.

계산 예시

예시 사례를 하나 보자. 피험자에게 2025년 3월 10일 사건이 발생해 입원했다. 기관은 3월 11일 이를 알게 되었고 3월 12일 의뢰자에게 통보했다. 이날이 0일이다. 의뢰자의 안전성 담당 의사는 이 반응을 관련 가능성 있음(possible)으로, 참고 안전성 정보에 없는 것으로 평가했다.

SDK와 CLI가 실제로 하는 일

recordAdverseEvent는 reportingRequired = ae.seriousness.serious로 정하고, 참이면 startDate에 7일(사망 또는 생명 위협) 또는 15일을 더한다. 위 예시라면 인과관계나 예상성과 상관없이 모든 중대한 사건에 대해 발생일을 기준으로 입원 사례에는 3월 25일, 생명을 위협한 사례에는 3월 17일을 reportingDeadline으로 돌려준다. 발생 직후라면 결과가 단지 보수적일 뿐이지만, 발생 몇 주 뒤에 발견된 사건이라면 계산된 기한이 기록을 저장하기도 전에 이미 지나 있다. 이 메서드는 SAE_NOT_REPORTED(B004)도 발생시키지 않는다. 이 코드는 오류 열거형에 있지만 SDK 어디에서도 쓰이지 않는다. 또 collectPatientData는 요청 타입에 adverseEvents 배열을 받게 되어 있으나 실제로 처리하지 않으므로, 이 경로로 들어온 이상사례는 보고 평가를 전혀 받지 않는다.

타입에도 빈틈이 있다. 독립 함수 recordAdverseEvent()는 반환 타입이 일반 AdverseEvent로 선언되어 있어서, 실행 시에는 값이 들어 있어도 TypeScript 호출자는 reportingRequired와 reportingDeadline을 볼 수 없다. 이 값이 필요하면 ClinicalTrialSDK 메서드를 써야 한다.

CLI 명령 report-ae는 시연용 출력기다. 인자가 무엇이든 같은 사건(Hypoglycemia, PT 10020993, 인과관계 "Probable", 결과 "Recovered")을 출력한다. --severity 인자가 "severe"나 "life-threatening"이면 SAE로 분류하는데, 이는 강도를 중대성처럼 쓰는 것이다. 그리고 "life-threatening"이어도 항상 오늘 날짜로부터 15일 기한을 출력한다. 이 출력으로 무엇을 판단해서는 안 된다.

수정한 시한 계산

아래 함수는 안전성 담당 의사가 판단하는 순서, 즉 중대한가, 의심되는가, 예상하지 못한 것인가, 사망·생명 위협인가의 순서대로 결정을 구현한다. v1.0 모델에 없는 두 입력, expectedness와 sponsorAwareDate가 필요하며, 날짜만 있는 값을 UTC 기준 역일로 세어 시간대 때문에 기한이 하루 밀리는 일이 없게 한다.

type Causality = 'certain' | 'probable' | 'possible' | 'unlikely' | 'unassessable';
type Expectedness = 'expected' | 'unexpected';

interface SafetyCase {
  serious: boolean;
  death: boolean;
  lifeThreatening: boolean;
  causality: Causality;
  expectedness: Expectedness;   // 참고 안전성 정보(RSI) 기준
  sponsorAwareDate: string;     // 0일, ISO 8601 날짜
}

// 문서화된 관행: 'unassessable'은 명확해질 때까지 관련 있음으로 본다.
const RELATED: Causality[] = ['certain', 'probable', 'possible', 'unassessable'];

function addDays(isoDate: string, days: number): string {
  const [y, m, d] = isoDate.split('-').map(Number);
  const t = Date.UTC(y, m - 1, d) + days * 86_400_000;
  return new Date(t).toISOString().slice(0, 10);
}

function expeditedClock(c: SafetyCase) {
  if (!c.serious) return { expedited: false, reason: 'not serious' };
  if (!RELATED.includes(c.causality)) return { expedited: false, reason: 'not suspected' };
  if (c.expectedness === 'expected') return { expedited: false, reason: 'expected (listed in RSI)' };
  if (c.death || c.lifeThreatening) {
    return {
      expedited: true,
      reason: 'fatal/life-threatening SUSAR',
      initialDue: addDays(c.sponsorAwareDate, 7),
      followUpDue: addDays(c.sponsorAwareDate, 7 + 8),
    };
  }
  return { expedited: true, reason: 'other SUSAR', initialDue: addDays(c.sponsorAwareDate, 15) };
}

const base = { serious: true, death: false, lifeThreatening: false,
  causality: 'possible' as Causality, expectedness: 'unexpected' as Expectedness,
  sponsorAwareDate: '2025-03-12' };

console.log(expeditedClock({ ...base, lifeThreatening: true }));
console.log(expeditedClock(base));
console.log(expeditedClock({ ...base, expectedness: 'expected' }));
console.log(expeditedClock({ ...base, causality: 'unlikely' }));

Node.js 22에서 node --experimental-strip-types clock.ts로 실행하면 다음과 같이 출력된다.

{ expedited: true, reason: 'fatal/life-threatening SUSAR',
  initialDue: '2025-03-19', followUpDue: '2025-03-27' }
{ expedited: true, reason: 'other SUSAR', initialDue: '2025-03-27' }
{ expedited: false, reason: 'expected (listed in RSI)' }
{ expedited: false, reason: 'not suspected' }

이 함수는 규제기관 대상 질문에만 답한다. 모든 SAE는 여전히 즉시 의뢰자에게 전달되고 종결될 때까지 추적되어야 한다. 운영 시스템은 두 의무를 모두 관리하고 각 보고를 실제로 발송한 날짜를 기록해, 지연 보고를 찾아내고 사유를 설명할 수 있어야 한다.

4.7 이상사례 기록 전체 — JSON에서 SDTM AE와 ADAE까지

명세 §7.3 객체와 주석

{
  "aeId": "AE-001-015",            // SDK는 대신 "AE-<timestamp>-<random>"을 생성
  "subjectId": "STUDY01-001",
  "term": "Hypoglycemia",          // 원문, 변경하지 않음
  "meddraCode": {
    "pt": "10020993",
    "ptTerm": "Hypoglycaemia",
    "soc": "10027433",
    "socTerm": "Metabolism and nutrition disorders"
  },
  "startDate": "2025-03-15",
  "endDate": "2025-03-15",
  "ongoing": false,
  "severity": "mild",
  "seriousness": {
    "serious": false, "death": false, "lifeThreatening": false,
    "hospitalization": false, "disability": false,
    "congenitalAnomaly": false, "other": false
  },
  "causality": "probable",
  "action": "none",
  "outcome": "recovered"
}

(주석은 설명을 위해 덧붙인 것이며 JSON 문법에서는 허용되지 않는다.) SDK 타입과 비교하면 이 예시는 불완전하다. AdverseEvent는 trialId를 필수로 요구하지만 명세 객체에는 없으므로, 이 페이로드를 그대로 보내는 TypeScript 클라이언트는 타입 검사를 통과하지 못한다. 그 밖에는 기록 내부가 일관적이다. 중대하지 않고, 발생 당일 회복되었으며, 경증이고, 관련성이 상당히 확실하며, 시험약 투여는 바뀌지 않았다.

운영용 확장 기록

아래 버전은 v1.0 필드를 모두 유지하면서 이 장에서 필요하다고 논증한 필드를 추가한 것이다. 추가 필드는 제안으로 표시했으며 WIA-BIO-010 v1.0의 일부가 아니다. v1.0만 구현한 수신 시스템은 이 필드를 무시해도 된다.

{
  "aeId": "AE-001-015",
  "trialId": "STUDY01",
  "subjectId": "STUDY01-001",
  "term": "Hypoglycemia",
  "meddraCode": {
    "pt": "10020993", "ptTerm": "Hypoglycaemia",
    "soc": "10027433", "socTerm": "Metabolism and nutrition disorders",
    "lltTerm": "Hypoglycemia",          // 제안
    "meddraVersion": "26.0"             // 제안: 실제 사용한 버전
  },
  "startDate": "2025-03-15",
  "endDate": "2025-03-15",
  "ongoing": false,
  "severity": "mild",
  "ctcaeGrade": 1,
  "seriousness": {
    "serious": false, "death": false, "lifeThreatening": false,
    "hospitalization": false, "disability": false,
    "congenitalAnomaly": false, "other": false
  },
  "causality": "probable",
  "expectedness": "expected",           // 제안: 참고 안전성 정보 대비
  "sponsorAwareDate": "2025-03-18",     // 제안: 신속보고 시한의 0일
  "action": "none",
  "outcome": "recovered"
}

날짜, CTCAE 등급, 예상성 값은 예시다. LLT 코드는 일부러 넣지 않았다. LLT 코드는 책이 아니라 사용 중인 버전의 라이선스 사전에서 가져와야 한다.

기록을 받아들이기 전의 일관성 검사

4.2절과 4.4절에서 논증한 규칙은 기계적으로 강제할 수 있다. 아래 검사기는 문제 목록을 돌려준다. 빈 목록이면 기록을 저장해도 되고, 그렇지 않으면 목록을 details에 담아 VALIDATION_FAILURE(B003)로 처리한다.

type AESeverity = 'mild' | 'moderate' | 'severe' | 'life-threatening' | 'death';
type AEOutcome = 'recovered' | 'recovering' | 'not-recovered'
  | 'recovered-with-sequelae' | 'fatal' | 'unknown';

interface AeLike {
  ongoing: boolean;
  endDate?: string;
  severity: AESeverity;
  outcome: AEOutcome;
  ctcaeGrade?: 1 | 2 | 3 | 4 | 5;
  seriousness: {
    serious: boolean; death: boolean; lifeThreatening: boolean; hospitalization: boolean;
    disability: boolean; congenitalAnomaly: boolean; other: boolean;
  };
}

function checkAe(ae: AeLike): string[] {
  const issues: string[] = [];
  const s = ae.seriousness;
  const anyCriterion = s.death || s.lifeThreatening || s.hospitalization
    || s.disability || s.congenitalAnomaly || s.other;
  if (s.serious !== anyCriterion) {
    issues.push(`serious=${s.serious} but criteria say ${anyCriterion}`);
  }
  if (s.death !== (ae.outcome === 'fatal')) {
    issues.push('seriousness.death and outcome "fatal" disagree');
  }
  if (ae.severity === 'death' && ae.outcome !== 'fatal') {
    issues.push('severity "death" used without a fatal outcome');
  }
  if (ae.ctcaeGrade === 5 && ae.outcome !== 'fatal') {
    issues.push('CTCAE grade 5 requires outcome "fatal"');
  }
  if (ae.ongoing && ae.endDate) issues.push('ongoing event must not have endDate');
  if (!ae.ongoing && !ae.endDate) issues.push('resolved event needs endDate');
  return issues;
}

§7.3 기록에 적용하면 checkAe는 []를 돌려준다. 이 기록을 복사해 hospitalization과 ongoing을 true로 바꾸되 serious는 false로, 종료일은 그대로 두면 다음을 돌려준다.

[
  'serious=false but criteria say true',
  'ongoing event must not have endDate'
]

첫 번째 메시지가 바로 SDK라면 보고 대상이 아닌 것으로 분류했을 경우다. 이 검사기는 중대성 플래그 death와 결과 fatal을 같은 것으로 본다는 점에 유의한다. 엄밀히 말하면, 한 피험자에게서 사망이라는 결과가 다른 사건에 귀속되면서도 어떤 SAE가 사망 때문에 중대하다고 분류될 수 있다. 이를 허용하는 계획서라면 규칙을 완화하고 사건 간 연결을 명시적으로 남겨야 한다.

SDTM AE로의 대응

SDTM AE 도메인은 피험자별·사건별로 한 행을 두며, 규제기관이 받는 표 형태다. 아래 표는 확장 기록을 옮긴 것이다. 통제 용어 값은 CDISC 관행을 따르며, AEREL 값은 의뢰자가 정의한다.

SDTM 변수값WIA 기록의 출처
STUDYIDSTUDY01trialId
DOMAINAE상수
USUBJIDSTUDY01-001subjectId(이미 시험 접두어를 포함함, 제3장 참조)
AESEQ1내보내기 시 피험자별로 부여
AETERMHypoglycemiaterm
AELLTHypoglycemiameddraCode.lltTerm(제안)
AEDECOD / AEPTCDHypoglycaemia / 10020993ptTerm / pt
AEBODSYS / AEBDSYCDMetabolism and nutrition disorders / 10027433socTerm / soc(일차 SOC)
AESEVMILDseverity
AETOXGR1ctcaeGrade
AESERNseriousness.serious
AERELPROBABLEcausality(의뢰자 정의 값)
AEACNDOSE NOT CHANGEDaction: 'none'
AEOUTRECOVERED/RESOLVEDoutcome: 'recovered'
AESTDTC / AEENDTC2025-03-15 / 2025-03-15startDate / endDate

두 가지 대응은 주의가 필요하다. SDK 값 action: 'none'은 모호하다. "투여를 변경 없이 계속함"(DOSE NOT CHANGED)일 수도 있고 "투여 중인 시험약이 없었음"(NOT APPLICABLE)일 수도 있으므로, 수집 양식에서 둘을 구분해야 한다. 또 중대한 사건이라면 여섯 기준 플래그가 각각 자체 Y/N 변수(AESDTH, AESLIFE, AESHOSP, AESDISAB, AESCONG, AESMIE)로 옮겨진다. serious를 기준에서 도출하는 것이 중요한 이유가 여기에 있다. 내보낸 데이터셋에서 불일치가 그대로 드러나기 때문이다.

SDTM AE에서 ADAE로

분석 데이터셋 ADAE는 통계 담당자가 안전성 표를 만드는 데 필요한 것을 덧붙인다. 핵심 도출 변수는 치료 후 발현 플래그 TRTEMFL이다. 첫 투여일 또는 그 이후에 시작했거나 첫 투여 후 악화된 사건으로서, 통계분석계획서가 정한 투여 후 기간 안에 있으면 치료 후 발현 사건이다. 예시에서는 피험자의 첫 투여일을 2025년 1월 20일로 가정한다(ADSL의 TRTSDT에서 가져올 예시 날짜다).

ADAE 변수값도출
ASTDT2025-03-15AESTDTC의 숫자 날짜, 부분 날짜면 대체 규칙 적용
ASTDY55ASTDT − TRTSDT + 1(3월 15일은 1월 20일로부터 54일 뒤)
TRTEMFLY첫 투여일 또는 그 이후 발생
AEDECOD, AEBODSYSAE와 동일SOC·PT별 요약을 위해 그대로 가져옴
ARELRELATEDAEREL에 의뢰자의 이분화 규칙 적용(4.5절)

표준 안전성 요약표, 즉 치료군별로 SOC와 PT에 따라 치료 후 발현 이상사례를 한 건 이상 겪은 피험자 수와 비율은 TRTEMFL = 'Y'인 ADAE 행에서 용어마다 피험자를 한 번씩만 세어 계산한다. 피험자 대신 사건 수를 세거나 LLT 단위로 요약하는 것은 흔한 오류이며, 위 구조는 그런 오류를 막도록 설계되어 있다.

4.8 장 점검표와 알려진 결함

점검 항목근거"완료"의 모습
중증도와 중대성을 따로 저장하고 서로에게서 추론하지 않음명세 §5.2, §5.4, 이 장 4.1절severity로 중대성 플래그를 설정하는 코드 경로가 없다.
serious를 여섯 기준에서 도출SeriousnessCriteria불일치 기록은 B003으로 거부한다.
여섯 번째 기준을 "중요한 의학적 사건"으로 구현21 CFR 312.32, 명세 문구 수정other에 규정상 정의를 문서화했다.
원문 보존, LLT로 코딩하고 PT·일차 SOC 도출명세 §5.1, §7.3AETERM이 수집된 원문과 같다.
MedDRA 버전 기록과 재코딩 절차 마련명세 §10은 26.0 인용기록 또는 시험 단위로 버전을 저장하고, 버전 상향 검토를 기록한다.
3점 중증도와 CTCAE 등급을 별도 필드로 유지AESeverity, ctcaeGradeseverity는 mild/moderate/severe로 제한한다.
인과관계 이분화 규칙을 안전성 관리 계획에 명시명세 §5.3unassessable을 포함해 모든 사례에 같은 규칙을 적용한다.
예상성과 의뢰자 인지일 수집v1.0에 없음의심되고 예상하지 못한 중대한 반응에 한해 0일부터 시한을 계산한다.
신속보고 시한과 별개로 모든 SAE를 즉시 의뢰자에게 전달ICH E6, 21 CFR 312.64(b)기관→의뢰자 전달 시각을 저장한다.

v1.0 참조 자료의 알려진 결함(안전성 데이터)

안전성 데이터까지 모델링하면 이 책의 데이터 형식 부분이 끝난다. 제5장은 프로그래밍 인터페이스 자체, 즉 @wia/bio-010 SDK와 CLI 전체로 넘어가 같은 방법을 적용한다. 각 호출이 무엇을 약속하는지, 실제로 무엇을 하는지, 그리고 빈틈이 있어도 어떻게 안전하게 쓸 수 있는지를 살핀다.