Internal Tools · React 18 · Mantine UI · Firebase

RED Toolkit

경기혈액원 단체 헌혈 운영에서 실제로 쓰던 데스크탑 도구 3종 묶음. 헌혈 주기 계산, 학교 시간표 조회, 단체별 헌혈 인원 예측까지 — 모두 업무 흐름에서 반복적으로 필요한 작업을 빠르게 처리하기 위해 설계했다.

용도
대한적십자사 경기혈액원 단체 헌혈 실무 (내부 사용)
프론트
React 18 · Vite 7 · Mantine UI v8 · lucide-react · dayjs
백엔드
Firebase Cloud Functions · Firestore · IndexedDB (로컬 캐시)
예측 엔진
순수 JS — 외부 ML 라이브러리 없음. Heuristic → Ridge 회귀 자동 전환
외부 연동
comcigan-parser (학교 시간표)

1. 화면

3개 도구를 사이드바 토글로 전환한다. 활성 도구는 localStorage에 저장되어 새로고침해도 유지.

RED Toolkit 8주 계산기 — 캘린더에 다음 헌혈 가능일 표시
8주 계산기 — 마지막 헌혈일 + 56일 후 다음 헌혈 가능일을 캘린더에 표시. mock Cloud Functions
RED Toolkit 시간표 조회 — 학교 목록에서 학교 선택 → 학년/반/요일/교시 시간표
시간표 조회 — 경기도 고등학교 목록(mock)에서 학교 선택 시 학년/반/요일/교시별 시간표 표시. Cloud Functions getSchools + getTimetable 응답 mock
RED Toolkit 단체 분석·예측 — 7개 단체 카드, 차트, Ridge 회귀 예측
단체 분석·예측 — 등록 단체 7개(일반단체/공공기관/대학교/고등학교/군부대) 분포, 회당 평균 차트, Ridge 회귀 기반 다음 행사 헌혈 인원 예측

2. 세 가지 도구

2.1 8주 계산기 (EightWeekCalculator)

전혈 400mL 헌혈의 법적 최소 재헌혈 간격은 56일(8주)이다. 기존에는 직원들이 손으로 세 날짜를 세야 했다 — 단체 행사의 경우 단체별·차수별로 또 다시 계산해야 해서 빈번한 실수가 발생했다. 헌혈일 + 주기 → 다음 헌혈 가능일을 즉시 보여주는 캘린더 UI. 혈장·혈소판 성분헌혈의 별도 주기도 적용.

2.2 학교 시간표 (SchoolTimetable)

학교 단체 헌혈은 학년/반별 수업 시간을 알아야 배차 스케줄을 짤 수 있다. 컴시간 파서(comcigan-parser)로 학교 코드를 입력하면 학년·반·요일·교시별 수업 시간을 조회. "5학년 2반이 수요일 6교시에 비는가?" 같은 질문에 즉시 답한다. 학교 시간표 캐싱은 IndexedDB에 저장해 오프라인에서도 조회 가능.

2.3 단체 분석·예측 (OrganizationAnalysisTool)

핵심 도구. 등록된 단체의 헌혈 이력을 시각화하고 다음 행사 헌혈 인원을 예측한다. CSV 업로드 / 수동 입력 두 경로로 기록을 추가할 수 있고, 17개 시도 지역·단체 구분(직장/학교/군부대/지역사회)별로 점수화.

3. 예측 시스템 — Ridge 회귀 (Phase 2)

3.1 곱셈식 예측 공식

RED Toolkit 예측 공식 — Base × S_seasonal × S_dow × S_cycle × S_global × S_local
Heuristic 모드: 6개 팩터의 곱으로 예측

3.2 CycleFactor — 단순 경과일 비교가 아닌 시그모이드

8주가 지났는지로 자르면 12주 주기 단체는 8주 시점에 인위적으로 잘라낸다. RED Toolkit은 단체의 실제 평균 주기 T_cycle 대비 경과일로 시그모이드를 계산한다.

S_cycle = 1 / (1 + e^(-k × (d / T_cycle - 1)))
  k = 5 (감도 파라미터)
  d / T_cycle = 1 일 때 S_cycle = 0.5
  d가 커질수록 1에 수렴 (떨어지지는 않음)

3.3 Ridge 회귀 — 5가지 시각으로 보기

데이터가 300건 미만이면 위 곱셈식(modelType: 'heuristic')을 그대로 쓰고, 300건 이상이면 closed-form Ridge 회귀로 팩터 가중치를 자동 튜닝한다. 같은 아키텍처를 수식 · 흐름도 · 행렬 기하 · 실제 코드 · 한계 5가지 방식으로 풀어본다.

(a) 수식 — log-선형 Ridge

Base만 로그 변환해 곱셈 관계를 가산 관계로 바꾸고, 나머지 팩터는 선형 결합. 정규화 항 αI로 과적합을 제어한다. α=0.1은 기본값 — 데이터 1,000건 단위에서 안정적.

ln(ŷ) = w₀ + w₁·ln(Base)
            + w₂·S_seasonal + w₃·S_dow
            + w₄·S_cycle    + w₅·S_global
            + w₆·S_local

  w* = (XᵀX + αI)⁻¹ Xᵀy,    α = 0.1
  ŷ  = exp( ... )

(b) 흐름도 — 학습/예측 한 사이클

RED Toolkit Ridge 회귀 학습 흐름 — 학습 데이터 구성 → 설계 행렬 X → Ridge 가중치 → 모델 결과 → 예측 적용
trainRidgeModel() (300건 이상) → Ridge closed-form → predictDonors()에서 ŷ 산출

(c) 행렬 기하 — Ridge는 정규화된 최소제곱

표준 OLS는 Xw ≈ y 를 풀지만, 변수 간 다중공선성이 있으면 XᵀX 가 특이행렬에 가까워져 해가 불안정해진다. Ridge는 XᵀX + αI 로 대각선을 부풀려 안정적인 역행렬을 보장한다.

  XᵀX           = Σᵢ xᵢ · xᵢᵀ    (p×p 공분산 행렬)
  Xᵀy           = Σᵢ xᵢ · yᵢ    (p×1)
  (XᵀX + αI)⁻¹                  (정규화 후 역행렬, 항상 존재)

  순수 JS 구현: invertMatrix() 가우스-소거법으로 직접 계산
                → npm 의존성 없음

(d) 실제 코드 — Ridge 핵심 (utils/donationPrediction.js)

// Ridge closed-form
const ridgeRegression = (X, y, alpha = 0.1) => {
  const Xt  = transpose(X)
  const XtX = matMul(Xt, X)              // p×p
  for (let i = 0; i < XtX.length; i++)   // 대각선에 α 더하기
    XtX[i][i] += alpha

  const inv = invertMatrix(XtX)          // Gauss-Jordan 소거 (외부 lib 없음)
  if (!inv) return null

  const Xty = Xt.map(row => row.reduce((s, v, i) => s + v * y[i], 0))
  return inv.map(row => row.reduce((s, v, i) => s + v * Xty[i], 0))
}

// 학습 데이터 — leakage 방지로 record 이전 데이터만 사용
for (const record of allRecords) {
  const prevRecs     = orgRecs.filter(r => r.실시날짜 < record.실시날짜)
  const prevAllRecs  = allRecords.filter(r => r.실시날짜 < record.실시날짜)
  const base         = org.회당평균
  const seasonal     = calcSeasonalFactor(month, year, prevAllRecs)
  const dow          = calcDayOfWeekFactor(dayOfWeek, prevRecs)
  const cycle        = calcCycleFactor(lastDate, record.실시날짜, org.주기평균 || 30)
  const globalTrend  = calcGlobalTrendFactor(record.실시날짜, prevAllRecs)
  const localTrend   = calcLocalTrendFactor(prevRecs)

  X.push([1, Math.log(Math.max(1, base)), seasonal, dow, cycle, globalTrend, localTrend])
  y.push(Math.log(Math.max(1, record.헌혈인원)))
}
      

leakage 방지: record.실시날짜 이전 데이터만으로 팩터를 계산한다. 미래 정보를 학습 데이터에 섞지 않기 위함 — 안 그러면 backtest 성능이 과대평가된다.

(e) 한계 — Ridge가 풀지 못하는 것

결과 객체에 factors · weights · modelType · confidence · formulaText 가 모두 포함되어, 툴팁에서 "왜 이 예측값이 나왔는가"를 직원이 이해할 수 있다.

4. 데이터 현황과 향후 (Phase 3)

Ridge 회귀(현재) MAPE ~25%, 단순 GBT 도입 시 ~18%, 앙상블 GBT ~12%, 전국 통합 모델 ~8%를 데이터 요구량 기준으로 단계화.

5. 구현 디테일

6. 레포지토리

red-toolkit React · Mantine · Firebase · Private

3개 도구 (8주 계산, 학교 시간표, 단체 분석/예측) + Ridge 회귀 예측 엔진 + 로드맵 문서.

a7garden/red-toolkit

7. 관련 문서