尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
직업성 질환 통합 평가 시스템 (wr-evaluation-unified)查看完整 README ↗
Version: 7.1.4 | Status: 인트라넷 운영 중 · 보안 핫픽스 7.1.4 — 워크스페이스 저장의 담당의·revision·편집 락 검사 + 임시 비밀번호 계정의 전체 API 차단 + Electron IPC·권한·내비게이션 가드 + AI 3경로 비식별 구조화 입력(웹은 Origin·토큰·Upstash 속도 제한) + 업데이트 피드 Ed25519 서명(셸이 서명 검증 후에만 다운로드, 패키징에 서명 단계) +
/apino-store + multer 2.x·xlsx 0.20.3(DB 마이그레이션 없음, Electron 설치본 교체 필요) · 제한 데이터 열람 권한자의 소수 셀 제한 해제를 회귀·이변량·상관행렬·예측으로 확장(상관행렬·예측은 실행 시 해제본 저장, DB 마이그레이션 0037) + 예측 추정 불가 시 완전사례 기준 인원 표시 · 무릎 노출 변수 4종(쪼그려앉기 분/일·시간, 중량물 kg/일·톤)을 직업력 단순합에서 직력 기간 가중평균·누적으로 개편 + 대표 직종명을 상병(disease) grain에서 선택 + 제한 데이터 열람 권한자에게 기술통계 소수 셀 제한 응답 시점 해제(DB 마이그레이션 0036, 카탈로그 v31) · 직력별 "신체부담평가 미포함" 선택(직력 카드 포함/미포함 라디오, 모듈 평가·결과·통계의 신체부담 변수에서 제외, 직업력 출력에는 맨 끝에 표기) · EMR·엑셀 확인 상병은 종합소견에서 "확인"으로 입력한 상병만 내보내기 · 범주형 소수 범주 "기타" 병합 + 원본 범주 빈도 · 원본 히스토그램 경계를 보기 좋은 값으로 정렬(DB 마이그레이션 0035, 카탈로그 v29) · 통계 히스토그램 양 끝 소수 구간 병합 + 권한자 원본 히스토그램(권한 표시명 "제한 데이터 열람", DB 마이그레이션 0034) · 통계분석 워크벤치 도입(인트라넷 전용·기본 비활성STATS_WORKBENCH_ENABLED) — 기술통계·Table 1 층화·이변량 8종·상관행렬·회귀(OLS/이분 로지스틱, 진단·spline·interaction)·예측(L2 로지스틱) + 차트, 소수 셀 억제·차분 방지 공개통제, 비동기 실행 큐, 통합 카탈로그 85개 변수(case/job/disease grain), 서버가 데이터셋을 조립하는@wr/analytics-core공유 계산 패키지, Python 통계 엔진(별도 venv) · capability 권한(기본 3종 전원 허용, 제한 행데이터 필드만 grant) + 관리자 콘솔 "통계 권한" 탭 · 환자 완료시각 서버 재검증 추적 · DB 마이그레이션 0028~0033 · 상병 입력 진단명 잘림 수정 · 등록 마법사 필수값 안내 · 평가 시점 나이를 만 나이(재해일자 기준)로 정정 · 영상 분석 UI 가독성 개선 · 무릎 관절염 상병(M17 하위코드 전체·슬관절/관절염 표기)에서 K-L Grade 입력창이 숨던 문제 수정 · 손목 영상분석 timeout 오분류(DEADLINE_EXCEEDED) 수정 + dev 서버 포트 충돌 가드 · 영상분석 flat 참고 후보(무릎 비틀림·어깨/팔꿈치 반복·손목 반복/각도)에 골격 검수·"왜 이 값?" 근거 패널 배선 + Left/Right 반복시간 근거 패널 환산식 오류 수정 · 비담당 환자 조회 시 막혀 있던 조회 전용 토글(종합평가 그룹/개별 카드 전환·상병 칩 이동, 척추/경추 작업 목록 선택·직력 탭·MDDM/전신진동 탭, 영상분석 근거 패널 등) 상호작용 수정 · Electron 셸 자동 업데이트(electron-updater) 도입 — 관리자 on/off 스위치 + canary 채널, 평상시 휴면(트랙 2) · 종합소견 직접 편집 오버라이드(자동 생성 문장을 의사가 다듬어 EMR 전송) · 특이 사항 메모 개편(구 복귀 고려사항, EMR 전송 제외) · 락 취득/상실 직후 모듈 편집 가드 stale 클로저 수정 · 인증 부팅 시 무한 루프(자기영속 401 재시도) 수정 · 환자 등록일 표시 KST 타임존 버그 수정 · 등록번호 "유령 충돌" 해소 + 생년월일 정정 경로(정정 API·고아 person 해제·데드락 재시도) · 종합소견 그룹 형식 미리보기를 개별 형식과 통일 · zod record 타입 optional 처리에 맞춘 테스트 정리 · Electron 종료 후 로그인 유지 문제 + 서버 세션(logout/refresh) 동시성 경쟁조건 수정 · 환자 단위 편집 락(TTL lease lock, LOCK_ENFORCEMENT_MODE 롤아웃) 구현 · 경추/팔꿈치/손목 모듈 저장 무한루프 수정(stableStringify) · 경추 모듈 부담 작업 0건 시 완료 배지 오판정 수정 · 종합소견 미리보기 그룹/개별 탭 명칭 정리 + 토글 연동 자동전환 + 낮음 사유 그룹 제목 구분 표시 + 손목/팔꿈치 직업별 요약 가독성 개선 · 종합평가 패턴 그룹화(상병 50건↑ 우/좌 일괄 처리) + EMR byte 절감 + K-L/Ellman 조건부 표시 · 손목/팔꿈치 BK유형 그룹 합류 시 진단별 저장값 자동 동기화 수정 · 환자 목록 의사별 필터 + 비담당 환자 열람성(복사/스크롤) 개선 · EMR 디바이스 등록 rate-limit 자기악순환 방지 · EMR 추출(재해일자별 신청건 매칭·성별 자동입력)/직접입력 개선 · 일괄입력 빈 양식 상시 제공 · M4 영상분석 시범 운영(참고용, 미검증 배너 — 어깨 반복 시간합 계산기 + 공정별 값 표시 추가, 상세는 docs/VIDEO_ANALYSIS_IMPLEMENTATION_PLAN.md)
직업환경의학 전문의가 업무상 질병 인정 여부를 판단할 때 사용하는 통합 평가 도구. 무릎(슬관절), 척추(요추 MDDM(BK2108) + 전신진동 BK2110), 경추(목 BK2109), 팔꿈치(주관절 BK2101/2103/2105/2106), 어깨(견관절 BK2117), 손목(수관절 BK2113/2101/2103/2106) 평가를 지원하며, 향후 고관절 등을 플러그인 형태로 확장할 수 있다. 인트라넷 모드에서는 서버에 누적된 평가 데이터를 연구용으로 집계·분석하는 통계분석 워크벤치(v7.0.0)도 제공한다.
3가지 배포 형태 지원:
| 영역 | 기술 |
|---|---|
| 프론트엔드 | React 18, CSS Variables (다크모드 지원) |
| 빌드 | Vite 5 |
| 데스크톱 | Electron 22 + electron-builder (NSIS) |
| 웹 배포 | Vercel (서버리스) |
| AI | Google Gemini API (기본) + Claude API (Vercel 서버리스 / Electron IPC) |
| 내보내기 | xlsx (엑셀), html2pdf.js (PDF), jszip |
| 폰트 | Pretendard (CDN), Noto Sans KR (fallback) |
| 영역 | 기술 |
|---|---|
| API 서버 | Node.js 20 + TypeScript + Express |
| 데이터베이스 | PostgreSQL 16 (감사 로그 파티셔닝 포함) |
| 리버스 프록시 / HTTPS | Caddy 2 (내부 CA 자동 발급) |
| 컨테이너 | Docker Compose v2.17+ (profile 기반 backup 분리) |
| 백업 | pg_dump + GPG 암호화 (cron 자동 실행) |
| 백업 모니터링 | 별도 컨테이너 (stale 감지, alert 생성) |
| 인증 | JWT (access + refresh) + bcrypt, must_change_password 정책 |
| 감사 로그 | Ed25519 서명 (Electron 디바이스 키페어), append-only 파티션 |
| 디바이스 관리 | 인트라넷 PC 등록/승인 (admin 콘솔) |
| 통계 엔진 (v7.0.0) | Python 3 subprocess (app 이미지 내 별도 venv /opt/stats-venv: numpy 1.26.4 · scipy 1.13.0 · statsmodels 0.14.2 · pandas 2.2.3) — 별도 컨테이너 없음, 동시 실행 1건 |
| 공유 계산 패키지 (v7.0.0) | @wr/analytics-core (packages/analytics-core, TypeScript) — 6개 평가 모듈의 계산·변수 추출을 클라이언트와 서버가 같은 코드로 import |
| 권한 (v7.0.0) | capability 모델 (capabilities + user_capability_grants) — 기본 허용 권한 ∪ 유효 grant |
| 테스트 | Vitest (server/) + Vitest (renderer/, node 환경 기본 + jsdom·@testing-library/react·user-event로 컴포넌트/훅 렌더링 테스트, v6.1.6+) |
npm install # 의존성 설치
npm run dev # 개발 서버 (localhost:3000)
npm run build:web # 웹 빌드 → dist/web/
dev/build:*/test계열은shared/(@wr/contracts)와packages/analytics-core를scripts/prebuild-*.mjs로 먼저 빌드한다. 차트 호환성(Chrome 80 대응)은npm run verify:chart-compat로 확인한다.
npm run build:electron # Electron 빌드 → dist/electron/
npm run electron:dev # Electron 개발 실행 (standalone)
npm run electron:build # Electron 패키징 (standalone, NSIS)
# 1) 서버 측 — Docker Compose 기반 풀스택
# WR_GIT_COMMIT 없이 빌드하면 recipe 출처·완료 보고 빌드 식별자가 'unknown'으로 남는다.
WR_GIT_COMMIT=$(git rev-parse HEAD) docker compose up -d --build # 개발용
docker compose -f docker-compose.yml \
-f docker-compose.prod.yml \
--env-file .env.production -p wr-prod up -d # 프로덕션
# 2) 인트라넷 빌드 Electron 클라이언트
npm run electron:build:intranet # 인트라넷용 NSIS 인스톨러
# 3) 오프라인 배포 패키지 생성 (서버 이미지 + 클라이언트 인스톨러 + 설치 스크립트)
# 스크립트는 Electron 설치본을 빌드하지 않는다 — 2)를 먼저 실행해 release\latest.yml을 새로 만들어 둘 것
# (버전은 package.json에서 자동 인식하며, 설치본 버전이 다르면 중단한다)
.\scripts\export-offline-package.ps1
npm i -g vercel
vercel login
vercel --prod --scope <your-scope>
vercel env add GEMINI_API_KEY # Gemini AI 분석용 (기본)
vercel env add CLAUDE_API_KEY # Claude AI 분석용 (선택)
자세한 절차는 docs/OFFLINE_DEPLOYMENT_PACKAGE.md 참조:
export-offline-package.ps1 실행 → wr-evaluation-unified-{VERSION}-intranet.zip 생성install-prod.ps1 자동 실행 → Docker 이미지 로드 + 서비스 기동https://서버IP:8443) → device 승인기존 서버 업데이트: v6.x → v7.0.0은
docs/UPDATE_7.0.0.md참조(DB 마이그레이션 0028~0033 자동 적용 — 업데이트 전 백업 필수,WR_VERSION변경 필요). v5.0.x → v5.1.0은docs/UPDATE_5.1.0.md참조 (다운타임 ~10초, DB 변경 없음)
각 평가 모듈은 src/modules/<name>/index.js에서 registerModule()을 호출하여 자기 자신을 등록한다.
registerModule({
id: 'knee',
name: '무릎 (슬관절)',
icon: '🦵',
EvaluationComponent, // 메인 UI 컴포넌트
createModuleData, // 초기 데이터 팩토리
computeCalc, // 계산/점수 산출 함수
isComplete, // 완료 판정 함수
exportHandlers, // 내보내기 핸들러
tabs: [{ id: 'job', label: '신체부담 평가' }],
presetConfig: { // 프리셋 시스템 연동 (선택)
label, fields,
extractFromModule(moduleData, sharedJobId) { ... },
applyToModule(moduleData, sharedJobId, presetData) { ... },
},
});
새 모듈 추가:
src/modules/<name>/ 디렉토리 생성index.js에서 registerModule() 호출src/App.jsx에 import './modules/<name>' 한 줄 추가src/core/utils/diagnosisMapping.js에 ICD 코드 매핑 추가Patient
├── id, phase
└── data
├── shared ← 모듈 공통
│ ├── patientNo, name, gender, height, weight, birthDate, injuryDate
│ ├── hospitalName, department, doctorName
│ ├── medicalRecord, highBloodPressure, diabetes, visitHistory
│ ├── consultReplyOrtho/Neuro/Rehab/Other ← 다학제 회신
│ ├── diagnoses[] ← 상병 목록 { id, code, name, side }
│ └── jobs[] ← 직업력 { id, jobName, startDate, endDate, ..., excludeFromAnalysis? }
├── modules
│ ├── knee ← 무릎 전용 (jobExtras[], returnConsiderations)
│ ├── shoulder ← 어깨 전용 (jobExtras[], returnConsiderations)
│ ├── elbow ← 팔꿈치 전용 (jobEvaluations[], temporalSequence)
│ ├── wrist ← 손목 전용 (jobEvaluations[], temporalSequence)
│ ├── cervical ← 경추 전용 (tasks[] — sharedJobId로 직업 연결)
│ ├── spine ← 척추 전용 (tasks[] — sharedJobId로 직업 연결)
└── activeModules: ['knee', 'spine', 'shoulder', 'elbow', 'wrist', 'cervical']
[공유] 기본정보 → 상병 입력 → 모듈 선택
[무릎] 🦵 신체부담 평가
[팔꿈치] 💪 신체부담 평가
[손목] ✋ 신체부담 평가
[어깨] 🙆 신체부담 평가
[경추] 👤 부담 노출 평가
[척추] ⚕️ 신체부담 평가
[공유] 종합소견 → AI 분석
좌우 2패널 레이아웃:
한국 산재보상보험법 근골격계 질환 업무관련성 평가 기준 적용.
독일 산재보험 BK2101/2103/2105/2106 기반 공통 신체부담 평가. 임계값/스코어가 아닌 Gate-and-Flag 판정 방식.
| BK 유형 | 질환 |
|---|---|
| BK2101 | 상과병변 / 부착부 건병증 |
| BK2103 | 팔꿈치 골관절염 / 박리성 골연골염 |
| BK2105 | 팔꿈치 점액낭염 |
| BK2106 | 주관증후군 / 척골신경병변 |
jobEvaluations[] (직업 × 상병 2차원 entry) + 모듈 전체 공통 temporalSequence 1회 입력bkSelectionMode: auto | manual)work_pattern === 'continuous' 시 daily_share 임계값 상향(1.5h/20% vs 3h/40%)팔꿈치 모듈과 동일한 Gate-and-Flag 판정 방식을 따르며 손목/손가락 특화 지표(BK2113 등)를 추가로 수집하여 서술 형태 보고서를 자동화.
| BK 유형 | 질환 |
|---|---|
| BK2113 | 수근관 증후군 (손목터널증후군) |
| BK2101 | 건초염 (방아쇠수지, 드퀘르벵 등) |
| BK2103 | 손목 관절병증 (진동) |
| BK2106 | Guyon canal 증후군 / 압박성 신경병증 |
jobEvaluations[] (직업 × 상병 2차원 entry) + 모듈 전체 공통 temporalSequence 1회 입력독일 직업병 BK2117 기준 — 어깨 근골격계 질환 누적 노출 평가.
| 노출 유형 | 임계값 |
|---|---|
| 오버헤드 작업 | 3,600시간 |
| 반복동작 중간속도 | 38,000시간 |
| 반복동작 고도 | 9,400시간 |
| 중량물(≥20kg) | 200시간 |
| 손-팔 진동 | 5,300시간 |
독일 산재보험 BK2109 기준 — 경추 질환 부담 노출 평가. 팔꿈치/손목 모듈과 유사한 Gate-and-Flag 판정 방식.
tasks[] (sharedJobId로 직업 연결) — 척추(spine)와 동일 패턴main_task_name, load_weight_kg, carry_hours_per_shift 등) 추출/적용요추 압박력(MDDM, BK2108)과 전신진동(BK2110) 두 평가를 한 모듈에서 지원한다. 패널 상단 탭으로 편집 대상을 전환하지만, 두 평가는 독립적으로 수행·출력된다(종합소견·EMR·엑셀에 각각 별도 섹션). 각 평가는 3상태 토글(미평가/노출없음/노출있음)로 수행 여부를 관리하며, '노출있음(present)'일 때만 결과·산출물에 표시된다(MDDM 기본 '노출있음', 전신진동 기본 '미평가').
MDDM (Mainz-Dortmund Dose Model) 독일 직업성 요추 질환 평가 모델 적용.
D_r = √(Σ F²·t / 8h) · 8h (8시간 정규화 후 8시간 재곱, 단위 kN·h) — v5.1.3에서 원형 MDDM 공식과 단위 일치하도록 정정formulaVersion 플래그로 분기 — 기존 환자는 옛 공식 결과 그대로 유지, spine 작업을 실제로 추가/수정/삭제(드래그 reorder 포함)·프리셋 적용·BatchImport로 task 생성 시점에 v5.1.3으로 자동 승격되어 새 공식 적용| 기준 | 남성 | 여성 |
|---|---|---|
| DWS2 연구 기준 | 7.0 MN·h | 3.0 MN·h |
| 독일 법원 기준 | 12.5 MN·h | 8.5 MN·h |
| MDDM 기준 | 25 MN·h | 17 MN·h |
독일 BK2110(장기간 주로 수직 방향 전신진동 노출로 인한 요추간판 질환) 에너지형 진동노출 모델 적용. 간이 모드로, 진동가속도 aw를 최소~최대 범위로 입력받아 하한·상한 시나리오를 구간으로 산출한다(단일 대표축 단순화, k계수 생략).
A(8) = aw·√(T/8h), 직업 내 다구간은 에너지합 A(8) = √((1/8h)·Σ aw_i²·T_i). 일일 지표 Amax(8)(직업별 에너지합), 평생 누적용량 DV = Σ Amax(8)²·근무일수·근속연수(직업별 합산, Amax(8) ≥ 0.63일 때만 산입)Amax(8) ≥ 0.63 m/s², 평생 DV,RI = 1400 (m/s²)² — 구간이 기준을 걸치면 '걸침', 하한도 넘으면 '초과'. 위험도는 평생 DV 기준(보조 참고: 일일 조치값 0.5, z축 한계 0.8 m/s²)대시보드(건수·월별 추이 집계)와 별개로, 서버에 저장된 평가 데이터를 연구 목적으로 분석하는 화면. 인트라넷 모드 전용이며 기본 비활성이다. 클라이언트는 분석 요청(AnalysisRecipe)만 보내고, 서버가 조직 격리 스냅샷에서 데이터셋을 조립해 계산한다(브라우저가 만든 행은 서버가 검증할 수 없기 때문). 파생 공식은 클라이언트·서버가 같은 코드를 import하는 @wr/analytics-core로 일원화했다.
STATS_WORKBENCH_ENABLED=true + DEPLOYMENT_MODE=intranet + 통계 엔진 런타임 정상 → /api/config/public의 statsWorkbenchAvailable=true → 헤더·랜딩의 "통계분석" 버튼과 Electron 메뉴 **"통계분석 워크벤치"**가 노출된다. 하나라도 어긋나면 /api/stats/* 전체가 403이 아니라 404(기능 존재 자체를 숨김)다..env.production에 STATS_WORKBENCH_ENABLED=true를 넣고 재기동해야 켜진다. 켜지 않아도 서버 업데이트·마이그레이션은 동일하게 적용된다.판정 = 역할 기본값(default_all_roles) ∪ 유효(미만료·미회수) grant. 기본 허용 권한은 grant를 회수해도 막히지 않는다.
| capability | 기본 | 용도 |
|---|---|---|
stats.view | 조직 소속 인증 사용자 전원 | 카탈로그·레시피·결과 화면 열람 (GET /catalog, POST /preview) |
stats.regression | 전원 | 분석 실행·폴링·취소 (POST /analyze, GET /runs/:id, POST /runs/:id/cancel) — 기술통계·이변량·상관행렬·회귀·예측 공통 |
stats.export_results | 전원 | 저장된 집계 결과 CSV 내보내기 (POST /export) |
stats.export_limited_rows | grant 필요 | 관리자 콘솔 표시명 "제한 데이터 열람(원본 값·원본 분포)"(마이그레이션 0034→0035). 분석 응답에 제한 행데이터 필드(이상치 원값·산점도 원시 점·회귀 관측치 진단값·원본 히스토그램(구간 경계를 보기 좋은 정수·소수 단위로 정렬해 공개용과 다를 수 있음)·원본 범주 빈도)를 포함하고, 기술통계(담당의 층화 포함)의 소수 셀(고유 인원 1~9명) 비공개·"기타" 병합·결측 건수 연결 억제를 응답 시점에 해제(limitedDisclosure:'applied', 회귀·이변량도 같은 방식으로 해제(조회 시 재계산, 완전사례·EPV·반복측정 등 통계 조건은 유지)하고, 상관행렬은 원본 행을 남기지 않으므로 권한자가 실행할 때 워커가 엔진 1회로 일반본과 해제본을 함께 만들어 stats_runs.limited_result(마이그레이션 0037)에 저장해 조회자의 현재 권한으로 고르며(CSV 불포함), 예측도 같은 저장형 구조로 해제한다(공개통제 ②와 곡선의 소수 인원 구간 병합만 풀고 최소 50명·사건/비사건 각 25·EPV 등 추정 가능 조건은 유지 — 사건이 1~9명이면 억제 대신 "사건/비사건 인원 부족" 사유가 보인다). 해제 수치는 결과 캐시에 저장하지 않으며 재조회를 위해 원본(frozen_dataset)이 보존된 실행만 해제 가능 — 그 외는 재실행 필요). differencing 제한이 걸린 요청은 해제하지 않는다. 행 단위 CSV 내보내기는 아직 없음(집계 CSV는 저장된 집계 결과 그대로) |
stats.export_phi | admin 역할 + grant + step-up + 사유 | 예약됨 — 행 단위·PHI 내보내기는 후속(PR5)이라 현재 노출 경로 없음 |
grant는 관리자 콘솔 "통계 권한" 탭(/api/capabilities/*)에서 부여·회수하며 모두 감사 로그에 남는다.
| 모드 | 내용 | CSV |
|---|---|---|
| 기술통계 | 연속형(n·평균·SD·중앙값·사분위·왜도·첨도·히스토그램·박스플롯)·범주형 빈도(그 변수 하나만 분석하면 소수 범주를 "기타"로 합쳐 공개)·결측 사유 분포. Table 1 층화(담당의별/범주형 변수별, 소수 인원 "기타" 병합, 한 그룹이라도 억제되면 total도 억제) | 지원 |
| 이변량 | 실행 가능 8종: Welch t · Mann-Whitney · ANOVA · Kruskal-Wallis · 카이제곱 · Fisher exact · Pearson · Spearman. 반복측정 쌍 검정(paired t · Wilcoxon)은 카탈로그에만 있고 현재 실행 불가 | 미지원 |
| 상관행렬 | 변수 C(k,2)쌍 상관 일괄 계산 + BH-FDR 보정, 히트맵 | 미지원 |
| 회귀 | OLS · 이분 로지스틱(HC3 / person-cluster CR1 표준오차, 완전·준완전 분리 판정), 진단 플롯 4종·VIF·condition number, 자연 3차 spline · interaction · 표준화 | 지원 |
| 예측 | L2(ridge) 로지스틱 — person 단위 grouped stratified K-fold CV(반복 5회) + grouped bootstrap optimism 보정. ROC-AUC(CI)·PR-AUC·Brier·calibration, ROC/PR/calibration 곡선. 공식 점수·post_decision 정보는 서버가 predictor에서 차단 | 지원(집계, 계수 제외) |
결과 화면은 4열(카탈로그 · 레시피 · 결과 · Inspector/리포트)이며 차트는 SVG(히스토그램·박스플롯·수평 바·100% 누적 바·산점도·상관 히트맵·포레스트·곡선)다. 분석은 먼저 동기로 실행하고(기본 4초), 끝나지 않으면 202로 전환해 폴링하며 취소할 수 있다(비동기 큐: admission→claim→finish, 사용자·조직별 동시/시간당 한도).
CATALOG_VERSION = v29-exclude-from-analysis, 서버 통합 v29-exclude-from-analysis+v1-snapshot-columns) — 6개 평가 모듈 변수, 공통 인적사항·상병/직업 롤업, DB 컬럼(담당의·등록일, 필터 전용).case(사례, 환자 1행) · job(직력) · disease(상병). case 변수는 하위 grain으로 브로드캐스트되고, 한 사람이 여러 행이면 person 단위로 센다.stats_runs에 recipe/source/result digest·catalog/engine 버전과 함께 저장되며, 결과 보존 기간은 STATS_RUNS_RESULT_TTL_HOURS(기본 168시간)다.STATS_WORKBENCH_ENABLED=false · STATS_ENGINE_TIMEOUT_MS=30000 · STATS_ENGINE_KILL_GRACE_MS=2000 · STATS_ENGINE_MAX_CONCURRENCY=1 · STATS_MAX_CONCURRENT_ANALYZE_REQUESTS=4 · STATS_RUNS_RESULT_TTL_HOURS=168. 비동기 큐 노브(STATS_ASYNC_*)와 엔진 입출력 상한(STATS_ENGINE_*_MAX_BYTES)은 compose가 전달하지 않아 코드 기본값으로 고정된다. 통계 엔진은 app 컨테이너(메모리 6g·CPU 4 상한을 영상 분석과 공유) 안에서 실행되며 별도 컨테이너가 없다.
행 단위·PHI 내보내기와 step-up 재인증은 후속(PR5/PR5-B)이다. 예측은 내부 검증(grouped CV·bootstrap)까지이며 temporal holdout·subgroup 성능은 후속, 외부 검증은 별도 연구 대상이다. 이변량·상관행렬 CSV와 반복측정 쌍 검정은 미지원이다. 카탈로그의 남은 필드 대량 롤아웃도 진행 중이다.
통합 AI 분석 탭에서 Google Gemini(기본) 또는 Anthropic Claude를 선택하여 분석.
| 모델 | 특징 |
|---|---|
| Gemini 2.5 Flash (기본) | 빠름/저비용 |
| Gemini 2.5 Pro | 정밀 (thinking 기능 내장) |
| Claude Haiku 4.5 | 빠름/저비용 |
| Claude Sonnet 4.6 | 정밀 |
| 플랫폼 | 경로 | API 키 관리 |
|---|---|---|
| 웹 (Vercel) | POST /api/analyze → 서버리스 | 서버 환경변수 GEMINI_API_KEY / CLAUDE_API_KEY |
| Electron | window.electron.analyzeAI() → IPC | 사용자 입력 키 (설정 모달) |
각 Excel 버튼(현재/선택/전체)은 드롭다운으로 형식 선택 가능:
| 형식 | 모드 | 출력 |
|---|---|---|
| EMR 형식 | 현재/선택/전체 | EMR 소견서 .xlsx (선택/전체는 .zip) |
| 일괄입력용 | 현재/선택/전체 | flat table .xlsx (75열, roundtrip 지원) |
| 일괄입력 양식 | 상시 | 헤더만 있는 빈 양식 .xlsx (exportBatchTemplate) — 환자 0명에서도 항상 노출 |
| - | 보고서 미리보기 영역 PDF 변환 |
"일괄입력 양식" 버튼은 상단 툴바와 일괄 Import 모달("빈 양식 다운로드")에 항상 표시되어, 환자가 한 명도 없는 처음 사용자도 작성용 양식을 받을 수 있다.
일괄 Import: 엑셀 파일에서 복수 환자 일괄 등록 (드래그 앤 드롭, 무릎+어깨+척추 작업+팔꿈치 BK 엔트리+등록일 포함)
v7.1.4 기준 — 인트라넷 백엔드(환자 단위 편집 락 포함), 6개 신체 부위 평가 모듈, 영상 분석 파이프라인, 통계분석 워크벤치(
packages/analytics-core·services/stats-engine·/api/stats), EMR 연동 및 운영·배포 구성을 반영함.
wr-evaluation-unified/
├── docker-compose.yml # 기본 compose (dev + intranet 공통)
├── docker-compose.prod.yml # 프로덕션 오버레이 (앱은 127.0.0.1 루프백에만 게시, healthcheck)
├── .env.production.example # 프로덕션 env 템플릿
│
├── server/ # 인트라넷 API 백엔드
│ ├── Dockerfile
│ ├── migrations/ # 0001~0033 SQL migration (기동 시 자동 적용)
│ │ └── 0001_init.sql ... 0024_patient_locks.sql, 0025_session_persistent_cookie.sql, 0026_session_family_id.sql,
│ │ 0027_orphan_patient_persons.sql, 0028_capability_grants.sql, 0029_patient_completion_tracking.sql,
│ │ 0030_server_verified_completion.sql, 0031_stats_runs.sql, 0032_stats_runs_analysis_run_id.sql,
│ │ 0033_stats_runs_async_queue.sql
│ ├── src/
│ │ ├── index.ts # Express 진입점
│ │ ├── config.ts # 환경변수 검증
│ │ ├── middleware/
│ │ │ ├── auth.ts # JWT 검증
│ │ │ ├── audit.ts # writeAuditLog 헬퍼
│ │ │ ├── corsMiddleware.ts
│ │ │ ├── rateLimit.ts
│ │ │ ├── patientAccess.ts # 환자 접근 권한 검사
│ │ │ ├── csrf.ts, csp.ts # CSRF/CSP 보안 정책
│ │ │ └── adminOnly.ts
│ │ ├── routes/
│ │ │ ├── auth.ts # /api/auth (login, refresh, signup-request, change-password)
│ │ │ ├── patients.ts # /api/patients (CRUD + 충돌 감지)
│ │ │ ├── presets.ts # /api/presets (custom preset CRUD)
│ │ │ ├── workspaces.ts # /api/workspaces (자동저장/스냅샷)
│ │ │ ├── admin.ts # /api/admin (users, devices, audit, ops)
│ │ │ ├── audit.ts # /api/audit (POST 서명된 감사 로그 수신)
│ │ │ ├── devices.ts # /api/devices/register
│ │ │ ├── ai.ts # /api/ai (Gemini/Claude 프록시)
│ │ │ ├── opsStatus.ts # /api/ops/* (백업 상태, alert)
│ │ │ ├── videoAnalysis.ts # 영상 업로드·분석·결과 API
│ │ │ ├── stats.ts # /api/stats (catalog, preview, analyze, runs/:id, cancel, export) — v7.0.0
│ │ │ └── capabilityGrants.ts # /api/capabilities (catalog, grants, revoke) — v7.0.0
│ │ ├── jobs/ # 보존 정책·영상 클립 정리 잡
│ │ ├── workers/ # 영상 업로드, 레시피 검증, 분석 worker
│ │ ├── db/ # patient_persons, assigned_doctor 해결 등
│ │ ├── stats*.ts # 통계 워크벤치 서버 로직 (snapshot·dataset builder·카탈로그·공개통제·비동기 큐·엔진 호출·export)
│ │ └── cli/ # seedAdmin, runRetention, seedStatsTestPatients, runStatsRunCleanup
│ └── package.json
│
├── services/
│ ├── backup-monitor/ # 백업 stale 감지 + alert 컨테이너
│ │ ├── Dockerfile
│ │ ├── index.js
│ │ └── __tests__/isStale.test.js
│ ├── pose-inference/ # 영상 분석 Python 추론 (app 이미지 내 /opt/pose-venv)
│ └── stats-engine/ # 통계 Python 엔진 (v7.0.0, app 이미지 내 /opt/stats-venv)
│ ├── analyze.py, protocol.py # stdin/stdout JSON 프로토콜 (--selfcheck 지원)
│ ├── descriptive.py, bivariate.py, correlation_matrix.py, regression.py, prediction.py
│ └── requirements.txt # numpy/scipy/statsmodels/pandas 고정 버전
│
├── packages/
│ └── analytics-core/ # @wr/analytics-core (v7.0.0) — 클라이언트·서버 공유 계산·카탈로그
│ ├── modules/ # knee/spine/shoulder/elbow/wrist/cervical 계산·변수 추출
│ ├── catalog.ts, grainEntities.ts, completion.ts
│ └── coverage/ # 필드 coverage inventory (included/excluded_with_reason)
│
├── backup/
│ └── Dockerfile # backup 사이드카 (postgres:16-alpine + gnupg + cron)
│
├── caddy/
│ └── Caddyfile # HTTPS + 내부 CA 자동 발급
│
├── scripts/
│ ├── backup.sh # pg_dump → GPG 암호화 (alert 권한 fix 포함)
│ ├── restore.sh # 복호화 → pg_restore (GPG_PASSPHRASE 지원)
│ ├── audit-partition.sh # 감사 로그 월별 파티션 생성
│ ├── backup-crontab, partition-crontab
│ ├── export-offline-package.ps1 # 오프라인 패키지 생성 (v5.0.0 신규)
│ ├── import-images.ps1 / .sh # docker load 일괄
│ ├── install-prod.ps1 # Windows 자동 설치
│ ├── set-build-target.mjs # standalone/intranet 빌드 타깃 토글
│ ├── verify-csp.mjs # CSP 헤더 검증
│ ├── prebuild-shared.mjs, prebuild-analytics-core.mjs # @wr/contracts·@wr/analytics-core 선빌드
│ ├── verify-chart-compat.mjs # 통계 차트 구형 Chrome(80) 호환 검사
│ ├── mock-intranet-server.mjs # 개발용 mock 서버
│ ├── fetch-pose-weights.ps1 # pose inference 모델 준비
│ └── videoValidateReport.mjs # 영상 분석 검증 보고서
│
├── shared/ # contracts (server ↔ client 공유 타입, v5.0.0 신규)
│ └── contracts/
│ ├── auth.ts, patient.ts, preset.ts
│ └── index.ts
│
├── electron/ # standalone/intranet Electron 데스크톱
│ ├── main.js # 메인 프로세스
│ ├── preload-standalone.js # standalone 빌드용 preload
│ ├── preload-intranet.js # intranet 빌드용 preload (device 등록 IPC 등)
│ ├── audit.js # device 키페어 + Ed25519 서명
│ ├── auditQueue.js # 감사 로그 디스크 큐
│ ├── migrationGate.js # 인트라넷 모드 첫 진입 시 마이그레이션 게이트
│ ├── migrationDataReader.js # standalone 데이터 → 서버 마이그레이션
│ ├── paths.js, build-target.json
│ └── emr-helper/ # EMR 자동화 (C#)
│
├── src/ # 프론트엔드 (React)
│ ├── App.jsx
│ ├── index.css
│ ├── core/
│ │ ├── moduleRegistry.js
│ │ ├── auth/ # AuthContext, authChannel, session
│ │ ├── components/
│ │ │ ├── LandingScreen, IntakeWizard, PatientSidebar, MainHeader, ...
│ │ │ ├── LoginModal, ChangePasswordModal, AccountProfileModal
│ │ │ ├── AdminConsoleModal, SignupRequestModal
│ │ │ ├── ConflictResolveModal, MigrationReportModal
│ │ │ ├── VideoAnalysisStep, TargetPicker, SkeletonOverlay
│ │ │ ├── PresetSearch, PresetManageModal, PresetsSection
│ │ │ ├── BatchImportModal, SettingsModal, ...
│ │ │ ├── statistics/ # 통계분석 워크벤치 (StatisticsWorkbench, CatalogPanel, RecipePanel, ResultPanel, InspectorReportPanel)
│ │ │ └── charts/ # 통계 차트 SVG (ForestPlot, CurveChart, 히스토그램·박스플롯·산점도·히트맵 등)
│ │ ├── hooks/
│ │ │ ├── useAIAnalysis, useAIAvailable
│ │ │ ├── usePatientList, usePatientCrud
│ │ │ ├── usePatientSync # 인트라넷 환자 동기화
│ │ │ ├── useMigration # standalone → 서버 마이그레이션
│ │ │ ├── useServerConfig, useOpsStatus
│ │ │ ├── useWorkspacePersistence, ...
│ │ ├── services/
│ │ │ ├── presetRepository, patientRecords, workspaceRepository
│ │ │ ├── patientServerRepository, intranetWorkspaceRepository
│ │ │ ├── patientConflictResolution, localToServerMigrator
│ │ │ ├── httpClient, analysisClient, integrationStatus
│ │ │ ├── videoAnalysisClient, videoAnalysisRun, videoServerApply
│ │ │ └── videoValidation*, videoViewpoint*, videoProvenance
│ │ ├── utils/
│ │ │ ├── steps, data, diagnosisMapping, reportGenerator
│ │ │ ├── exportService, storage, platform, common
│ │ │ ├── csrfCookie, videoMapping
│ │ │ └── workspaceAutosavePolicy
│ ├── modules/
│ │ ├── knee, spine, shoulder, elbow, wrist, cervical
│ │ └── 각 모듈: Evaluation + components + utils + index.js
│
├── api/
│ └── analyze.js # Vercel 서버리스 AI 프록시
│
├── public/
│ ├── images/ # G1~G11 자세 이미지
│ └── job-presets.json # builtin 직업 프리셋
│
└── docs/ # 운영/배포 문서
├── PRD.md, README.md
├── OFFLINE_DEPLOYMENT_PACKAGE.md # 오프라인 설치 가이드 (v5.0.0)
├── PRODUCTION_RELEASE_PLAN.md # 운영 절차서
├── UPDATE_5.1.0.md # v5.0.x → v5.1.0 현장 업데이트 절차 (v5.1.0 신규)
├── UPDATE_7.0.0.md # v6.x → v7.0.0 현장 업데이트 절차 (마이그레이션·WR_VERSION·통계 활성화, v7.0.0 신규)
├── T46_GO_NO_GO.md # 리허설 결과표
├── T46_IMPLEMENTATION_PLAN.md
├── OPERATIONS_RUNBOOK.md
├── BACKUP_MONITORING_PLAN.md
├── INTRANET_DEPLOYMENT.md # HTTPS / 인증서 신뢰 등록
└── BACKUP_RESTORE.md
프론트엔드 모듈의 세부 구조는 다음과 같습니다.
src/ # (기존 standalone 부분만 발췌)
├── App.jsx # 메인 앱 진입점 (로직 분리 후 200줄 이하)
├── index.css # 글로벌 스타일 (CSS Variables)
├── core/ # 공유 프레임워크
│ ├── moduleRegistry.js # 모듈 등록/조회 API
│ ├── components/ # 핵심 UI 컴포넌트
│ │ ├── LandingScreen.jsx # 홈 화면 및 환자 목록
│ │ ├── IntakeWizard.jsx # 신규 환자 생성 3단계 위자드
│ │ ├── PatientSidebar.jsx # 환자 목록 사이드바 (검색/필터)
│ │ ├── MainHeader.jsx # 상단 헤더 (환자 정보, 설정)
│ │ ├── StepContent.jsx # 활성 스텝 콘텐츠 렌더러
│ │ ├── StepIndicator.jsx # 위자드 진행 표시기
│ │ ├── SaveLoadModals.jsx # 저장/로드 모달
│ │ ├── BasicInfoForm.jsx # 기본정보 입력 & 사이드패널
│ │ ├── DiagnosisForm.jsx # 상병 입력
│ │ ├── AssessmentStep.jsx # 모듈별 평가 스텝
│ │ ├── PresetSearch.jsx # 프리셋 검색/적용
│ │ ├── PresetManageModal.jsx # 프리셋 저장/삭제
│ │ ├── BatchImportModal.jsx # 일괄입력 (드래그&드롭)
│ │ ├── SettingsModal.jsx # 설정 모달 (AI 키, 폰트 등)
│ │ └── ...
│ ├── hooks/ # 사용자 정의 훅
│ │ ├── useEMRIntegration.js # EMR 통합 상태 & 동기화
│ │ ├── useExportHandlers.js # 내보내기 (Excel/PDF) 로직
│ │ ├── useIntakeWizard.js # 신규 환자 위자드 상태
│ │ ├── usePatientCrud.js # 환자 생성/조회/수정/삭제
│ │ ├── usePresetManagement.js # 프리셋 로드/저장/캐싱
│ │ ├── useStepNavigation.js # 스텝 네비게이션
│ │ ├── useWorkspacePersistence.js # 워크스페이스 자동 저장/복구
│ │ ├── useAIAnalysis.js # AI 모델 선택 (Gemini/Claude)
│ │ ├── usePatientList.js # 환자 목록 로드
│ │ └── useIntegrationStatus.js # EMR 연동 상태
│ ├── services/ # 비즈니스 로직 서비스
│ │ ├── presetRepository.js # 프리셋 CRUD (builtin+custom 병합)
│ │ ├── patientRecords.js # 환자 레코드 관리
│ │ ├── workspaceRepository.js # 워크스페이스 저장/복구
│ │ └── ...
│ ├── utils/ # 유틸리티 함수
│ │ ├── steps.js # buildSteps() — 동적 스텝 생성
│ │ ├── data.js # 데이터 초기화 & 마이그레이션
│ │ ├── diagnosisMapping.js # ICD 코드 → 모듈 매핑
│ │ ├── reportGenerator.js # 통합 미리보기 보고서
│ │ ├── exportService.js # 엑셀/PDF 내보내기
│ │ ├── patientCompletion.js # 환자 완료 판정
│ │ ├── storage.js # localStorage/Electron FS 추상화
│ │ ├── platform.js # 웹/Electron 플랫폼 분기
│ │ └── common.js # 공통 상수 & 헬퍼
│ └── auth/ # 인증 컨텍스트·세션·탭 동기화
├── modules/
│ ├── knee/ # 무릎 모듈 (한국 산재 기준)
│ │ ├── KneeEvaluation.jsx # 메인 컴포넌트
│ │ ├── components/ # JobTab, KneeResultPanel, AssessmentTab
│ │ ├── utils/ # calculations, data, exportHandlers
│ │ └── index.js # 모듈 등록
│ ├── spine/ # 척추 모듈 (MDDM)
│ │ ├── SpineEvaluation.jsx # 메인 컴포넌트
│ │ ├── components/ # TaskManager, TaskEditor, SpineResultPanel
│ │ ├── utils/ # calculations, formulaDB, thresholds, data
│ │ └── index.js # 모듈 등록
│ ├── shoulder/ # 어깨 모듈 (BK2117)
│ │ ├── ShoulderEvaluation.jsx # 메인 컴포넌트
│ │ ├── components/ # JobTab, ShoulderResultPanel
│ │ ├── utils/ # calculations, data, exportHandlers
│ │ └── index.js # 모듈 등록
│ ├── elbow/ # 팔꿈치 모듈 (BK2101/2103/2105/2106)
│ │ ├── ElbowEvaluation.jsx # 메인 컴포넌트
│ │ ├── components/ # ExposureForm, DiseaseSpecificFields, ElbowResultPanel
│ │ ├── utils/ # calculations, data, exportHandlers
│ │ └── index.js # 모듈 등록
│ ├── wrist/ # 손목 모듈 (BK2113/BK2101/2103/2106)
│ │ ├── WristEvaluation.jsx # 메인 컴포넌트
│ │ ├── components/ # ExposureForm, DiseaseSpecificFields, WristResultPanel
│ │ ├── utils/ # calculations, data, exportHandlers
│ │ └── index.js # 모듈 등록
│ └── cervical/ # 경추 모듈 (BK2109)
│ ├── CervicalEvaluation.jsx # 메인 컴포넌트
│ ├── components/ # TaskManager, TaskEditor, CervicalResultPanel
│ ├── utils/ # calculations, data, exportHandlers
│ └── index.js # 모듈 등록
ICD 코드 기반 모듈 자동 추천:
| ICD 코드 패턴 | 추천 모듈 |
|---|---|
| M17, M22, M23, M70.4, M76.5, S83 | 무릎 (knee) |
| M77.0, M77.1, T75.2 | 팔꿈치 (elbow) |
| G56.0, M65.3, M65.4, M65.8, M19.04 | 손목 (wrist) |
| M75, S43, S46, M19.01 | 어깨 (shoulder) |
| M50, M48.02 | 경추 (cervical) |
| M51, M54, M47, M48, M53 | 척추 (spine) |
| 우선순위 | 항목 | 설명 |
|---|---|---|
| P1 | 고관절 모듈 | hip 모듈 추가 (플러그인 패턴 활용) |
| P2 | ||
| P2 | ||
| P2 | 통합 PDF/Word | 통합 보고서를 PDF/Word 형식으로도 출력 |
| P3 | ||
| P2 | 통계 행 단위·PHI 내보내기 | stats.export_limited_rows/stats.export_phi 기반 행 단위 export + step-up 재인증 (PR5/PR5-B) |
| P2 | 통계 예측 확장 | temporal holdout·subgroup 성능 (외부 검증은 별도 연구) |
| P3 | 통계 카탈로그 잔여 롤아웃 | 남은 필드·롤업 후보 변수화 (현재 통합 83개) |
| P3 | 현장 device smoke | 실제 병원 PC에 인트라넷 인스톨러 배포 + 의료진 device 등록 검증 |
| P3 | 백업 자동 재해 복구 훈련 | 분기별 1회 복구 전용 환경에서 실제 복원 검증 |
v7.1.3(운영 배포 완료) 이후 병합된 보안 핫픽스(Phase H의 HF1~HF9)를 묶은 패치 릴리스다. 기능 추가 없이 보안 결함을 닫는 것이 목적이고, DB 마이그레이션은 없다(0036·0037은 7.1.3에 포함). 통계 계산·카탈로그·결과 스키마 버전도 바뀌지 않아 분석 캐시는 그대로다. 사용자에게 보이는 동작 변경은 아래 "동작 변경"에 모았다. 7.1.3과 달리 Electron 셸(main.js·preload) 변경이 있어 설치본 교체가 필요하다. 업데이트 피드 서명(HF8)은 처음에는 7.1.5로 나눌 계획이었으나, 셸 교체를 PC마다 두 번 하지 않도록 이 릴리스에 합쳤다.
workspace_patient_write_rejected)에 남는다.mustChangePassword 세션은 /api/auth/me·/api/auth/change-password 외 전부 403 PASSWORD_CHANGE_REQUIRED다. Electron은 이 상태에서 EMR 디바이스 등록 요청을 보내지 않아(IP 제한 소모 방지) 비밀번호 변경 직후 등록이 진행된다.analyze-ai·load-legacy-data·fs-*)을 아예 등록하지 않고, 모든 IPC 핸들러가 송신 창을 검증한다. 권한 요청은 clipboard-sanitized-write(관리자 콘솔의 임시 비밀번호 복사)만 허용하고 나머지는 거부하며, 새 창은 전부 차단하고 독립형 빌드는 내비게이션 가드(file://·개발 localhost·blob:만 허용)를 둔다./api 응답에 Cache-Control: no-store를 붙인다(환자 정보 응답이 중간 캐시에 남지 않게). 동작하지 않던 CSRF 예외 경로를 제거했고 .dockerignore를 보강했다.files: 1·fields: 0 제한을 둔다. 종결 경계 없이 끊긴 업로드가 임시 파일을 남기던 것도 해소된다(구 버전에서 실측). 정상 업로드 동작은 같다.cdn.sheetjs.com 타르볼, lock에 해시 고정)으로 교체했다. 같은 입력의 읽기·쓰기 결과를 두 버전에서 비교해 동일함을 확인했다. 빌드 환경이 cdn.sheetjs.com에 접근할 수 있어야 한다(npm ci가 타르볼을 받는다).@wr/contracts에 aiModels·aiAnalysis·aiPrompt 추가(동작 변경 없음)./api/ai/analyze가 { input, fullReport? }만 받는다. fullReport(보고서 전문, 60,000자 이하)는 내부 LLM 설정(AI_PROVIDER=internal)에서만 허용하고 외부 벤더 설정에서는 400으로 거부한다. 업스트림 오류 상세는 응답에 싣지 않고(502/504), 사용자당 분당 20회·AI_TIMEOUT_MS(기본 60초) 제한을 둔다. 운영은 AI_PROVIDER=none이라 이 경로는 내부 LLM 도입 전까지 휴면이다.v7.1.2 문서 반영(#148·#149) 이후 병합된 여섯 건을 묶은 패치 릴리스다. v7.1.2에서 기술통계에만 적용되던 제한 데이터 열람 권한자(stats.export_limited_rows)의 소수 셀(1~9명) 공개 제한 해제가 회귀·이변량·상관행렬·예측으로 확장되어 통계 워크벤치의 모든 분석에 적용된다. 권한이 없는 사용자의 화면·결과와 기존 통계 캐시는 바뀌지 않는다.
INSUFFICIENT_EVENT_PERSONS(사건/비사건 인원 부족) 사유가 보인다.stats_runs.result는 집계본 그대로). 해제 후 선택한 방법이 실행 불가로 드러나면(예: 반복측정) 집계본에 unavailable_method_not_executable 상태를 붙여 돌려준다(이변량은 대신 사유가 붙은 억제 결과를 해제본으로 보낸다). 회귀 점별 진단처럼 엔진을 한 번 더 부르는 단계 뒤에는 권한을 다시 확인하고, 회수됐으면 해제본·진단값을 버리고 일반 응답으로 돌려보낸다.requested_disclosure_profile='lift_eligible', execution_digest 입력이라 권한자·비권한자 캐시가 섞이지 않음)은 워커가 권한을 다시 확인한 뒤 엔진을 한 번만 돌려 일반본(result)과 해제본(limited_result, 마이그레이션 0037)을 함께 저장한다. 해제본은 일반 조회 컬럼 목록·캐시 적중·CSV 경로가 읽지 않으며, 조회는 조회자의 현재 권한 확인 → 해제본 조회 → 재확인 → strict 감사(실패 시 500, 일반본으로 강등하지 않음) 뒤에만 내보낸다. 접수 뒤 워커 시작 전에 권한이 회수되면 일반 억제 결과로 종결하고, 해제본 없는 행은 캐시에 남기지 않는다(cacheable=false).bivariate.unavailableReason(방법 사유 또는 엔진 계산 불가 사유), 상관행렬 억제 셀 reasonCode(REPEATED_MEASURES_NOT_ALIGNED·NOT_COMPUTABLE)를 카드·히트맵에 보여 준다. 일반 응답에는 붙지 않는다(소수 인원 때문인지 통계 불가인지 구분되지 않게 하는 기존 원칙 유지). 남은 억제 셀이 있으면 상관행렬의 보정 p값은 계속 비공개다.unavailable_source_missing) 다시 실행해야 한다.knee.relatedness.max 표시값이 구간 최댓값에서 구간 중점으로 바뀐(2026-09-26) 뒤 낡은 채 계속 실패하던 통합 테스트 3건의 기대값을, 하드코딩 대신 실제 계산 함수와 정의에서 독립 계산하도록 고쳤다. 서버 코드 변경은 없다.v7.1.1 문서 반영(#142) 이후 병합된 다섯 건을 묶은 패치 릴리스다. 통계 워크벤치의 무릎 쪼그려앉기·중량물 변수가 직업력 단순합에서 직력 기간 가중평균·누적으로 바뀌었고, 대표 직종명을 상병(disease) grain에서도 고를 수 있게 되었으며, 제한 데이터 열람 권한자는 기술통계의 소수 셀 제한을 응답 시점에 해제해 볼 수 있다. 운영에는 v7.1.1까지 배포되어 있고 이 버전이 다음 배포 대상이다.
knee.case.sumSquattingMinutesPerDay·knee.case.sumDailyLoadKg를 삭제하고 같은 방식의 변수 4개로 교체했다. 단순합은 직력이 많을수록 값이 부풀고 기간이 반영되지 않았다.
knee.case.weightedSquattingMinutesPerDay(분/일)·knee.case.weightedDailyLoadKg(kg/일) = Σ(노출값 × 종사 연수) ÷ Σ 종사 연수.knee.case.cumulativeSquattingHours(시간 = Σ 분/일 ÷ 60 × 연간 근무일 × 종사 연수)·knee.case.cumulativeLoadTon(톤 = Σ kg/일 ÷ 1000 × 연간 근무일 × 종사 연수). 예: 3000kg·2년 + 2000kg·8년(근무일 250) → 가중평균 2200 kg/일, 누적 5500톤.not_applicable, 노출값이 blank인 직력은 건너뛰며 입력한 0은 정상 0, 손상값·기간/근무일 invalid는 부분값 없이 not_entered+invalid, 노출값을 입력했는데 기간(누적은 근무일 포함)이 blank면 not_entered다. 종사 연수는 job.identity.tenureYears와 같은 엄격 규칙(jobTenure.ts)을 쓴다. 평가자 판정의 근거 입력값이라 예측 predictor로 허용하되 용도는 "판정 일관성 점검"이다.UNKNOWN_VARIABLE로 거부하고 화면이 대체 변수를 안내한다. 값의 의미가 달라 자동으로 바꾸지 않는다. 카탈로그는 analytics-core 83개(case 50 / job 19 / disease 14) + 서버 전용 2개 = 통합 85개.job.rollup.longestTenureJobNameNormalized는 유사식별자·고카디널리티라 case→job/disease 복제가 일괄 차단돼 있었다. 이 변수에만 broadcastToGrains: ['disease'] 변수별 예외를 두어 상병 grain에서 선택할 수 있게 했다(일반 규칙과 job grain 차단은 그대로, 허용 목적은 연관성만). 플래그는 카탈로그 DTO 계약으로 클라이언트에 전달된다. 기술통계 억제가 고유 인원 기준이라 상병 행 복제로 소수 셀이 우회되지 않는다.stats.export_limited_rows 권한자에게는 기술통계(일반 + 담당의 층화)의 범주별 억제·"기타" 병합·결측 연결 억제·total 강제 억제를 응답 시점에 해제한다. 캐시에는 집계 결과만 저장하고 해제는 조회자의 현재 권한으로 원본 행에서 다시 계산하며, 계산 뒤 공개·감사 직전에 권한을 재검사한다. 일반 사용자의 억제 규칙은 그대로다. 결과·미리보기 계약에 limitedDisclosure를 추가했고 마이그레이션 0036은 관리자 콘솔 권한 설명 문구만 갱신한다.updates/ 디렉터리를 포함하고(설치본·.blockmap·latest.yml), 콘솔 인코딩이 cp949일 때 한글 설치본 파일명이 깨져 설치본이 패키지에서 조용히 빠지던 문제를 고쳤다(검증기 출력을 ASCII로 고정, 복사 실패 시 중단).CATALOG_VERSION v31-knee-load-weighted-cumulative, RESULT_SCHEMA_VERSION v11-limited-disclosure, SUPPRESSION_RULE_VERSION v7-discrete-other-merge(변경 없음) — 기존 분석 실행 결과는 재사용되지 않고 새 규칙으로 다시 계산된다.v7.1.0 이후 병합된 다섯 건을 묶은 패치 릴리스다. 직력 단위로 신체부담 평가 대상을 고를 수 있게 되었고, EMR·엑셀의 "확인 상병" 칸이 신청 상병 복사에서 종합소견 입력 기준으로 바뀌었으며, 기술통계의 범주형·히스토그램 공개통제가 보강되었다.
신체부담평가 [포함] [미포함] 라디오를 두었다(기본 "포함"). 미포함 직력은 카드가 음영·점선으로 바뀌고 헤더에 배지가 붙으며, 입력 필드는 그대로 편집할 수 있다.
jobExtras·jobEvaluations·task·구간)은 지우지 않으므로 다시 "포함"으로 바꾸면 그대로 돌아온다(모듈 sync에는 항상 전체 직력을 넘기고 계산 결과 단계에서만 거른다). MDDM·전신진동은 sharedJobId가 없는 task·구간의 귀속(첫 직력)을 필터 전 전체 직력 기준으로 정한 뒤 미포함 직력에 귀속된 항목만 제외하며, 검증 메시지·최대값·합산·완료 판정이 같은 목록을 쓴다.txtJobCusCont/txtSyth1Cont의 [직업력]에는 미포함 직력도 남기되, 포함 직력을 먼저 두고 미포함 직력을 가장 끝에 신체부담평가에는 미포함을 붙여 출력한다(공통 함수 jobHistory.js로 세 곳을 통일). 포함 직력은 직력1..k로 모듈 블록의 직력 번호와 일치하며, 어깨 [직력별 기여]의 번호도 직업력 번호와 맞춘다. 미포함 직력이 없는 환자의 출력은 종전과 같다.shared.jobs와 함께 구형 직업 필드(modules.knee.jobs, modules.spine의 jobName·careerYears·careerMonths·workDaysPerYear)가 남은 환자는 직력과 연결을 확정할 수 없어 미포함 설정을 로드 시점에 해제하고 UI에서 선택을 막는다(migratePatient, 클라이언트 완료 판정, 서버 verifyAllModulesComplete, deterministicMigrate). 정상적으로 마이그레이션된 환자에게는 영향이 없다.(신체부담평가 미포함)을 표기한다.knee.job.*, shoulder.job.*)는 미포함 직력도 행(엔터티)을 유지한 채 값만 not_applicable로 반환한다(데이터셋 빌더의 엔터티:관측 1:1 검증 때문에 관측을 생략하지 않는다). case 집계(무릎·어깨 합계·relatedness·anyExceeded, 경추 누적부하·비중립시간, 팔꿈치·손목 부담등급, 척추 MDDM 선량·진동 DV)는 포함 직력만 반영하고, 전부 미포함이면 not_applicable(미입력 not_entered와 구분, 노출 상태 unknown의 not_assessed보다 먼저 판정)이다. 미포함 직력의 손상값이 포함 직력의 통계에 invalid로 남지 않도록 품질 검증을 계산에 쓴 목록 기준으로 맞췄고, 경추는 전체 직력 기준으로 task 귀속을 확정한 뒤 미포함 직력의 task만 제거한다. 직종명·근속 같은 직업력 정보 변수(job.identity.*)는 미포함 직력도 그대로 집계한다. 신체부담 변수 24곳의 dependsOn에 shared.jobs[].excludeFromAnalysis를 연결하고 을 로 올렸다.실서버 기술통계에서 히스토그램 대부분이 "분포를 표시하기에는 공개 가능한 구간이 부족합니다"로 억제되던 문제를 고쳤다. 균등분할의 끝 구간(치우친 분포의 꼬리, 극단값)에 1~9명만 있어도 재분할 사다리(최소 3구간)가 전부 실패했기 때문이다.
buildOriginalHistogram 하나가 집계 경로와 권한자 원본 모두를 만든다. 경계는 numpy linspace와 같은 연산 순서로 계산한다(기존 식은 [0,1]·5구간에서 0.6을 다른 구간에 배정). numpy 대조 픽스처 58케이스로 경계·건수 정확 일치를 고정했다. Python 엔진의 histogram 출력은 더 이상 쓰지 않는다(제거는 후속).rawHistogram): stats.export_limited_rows 권한자에게만 응답 시점에 붙인다(결과 캐시에 저장 안 함, 응답마다 감사 기록). 공개 조건은 변수 공개뿐이며 이상치 원값 게이트와 독립이다. 권한 회수는 다음 서버 응답부터 반영된다.stats.export_limited_rows 표시명을 "행 단위 제한데이터 내보내기"에서 **"제한 데이터 열람(원본 값·원본 히스토그램)"**으로 바꿨다(키 불변). 이 권한은 이상치 원값·산점도 원시 점·회귀 관측치 진단값·원본 히스토그램을 함께 연다. 행 단위 파일 내보내기는 여전히 없다.CHART_DISCLOSURE_POLICY_VERSION v3-histogram-tail-merge, RESULT_SCHEMA_VERSION v9-histogram-tail-merge-raw — 기존 분석 실행 결과는 재사용되지 않고 새 규칙으로 다시 계산된다(버전이 다른 과거 실행 조회는 저장된 집계만 반환)..env.production의 WR_VERSION=7.1.0 ③ 배포 후 원본 히스토그램을 볼 사용자에게 관리자 콘솔 "통계 권한" 탭에서 "제한 데이터 열람(원본 값·원본 히스토그램)" 부여. Electron 재설치는 필요 없다(서버 SPA 로드).statsBivariateHttp welch_t 2건·statsRegressionHttp 표준화 1건 통합 테스트 실패는 v7.0.0(main)에서도 동일하게 재현되는 기존 문제다(knee.relatedness 평균 변경 후 기대값 미갱신 추정) — 별도 수정 예정.인트라넷 서버에 누적된 평가 데이터를 연구용으로 집계·분석하는 통계분석 워크벤치를 도입했다(PR0-A~PR4-B2 + 폴리싱, 29개 PR). 클라이언트는 AnalysisRecipe만 보내고 서버가 조직 격리 스냅샷에서 데이터셋을 조립하며, 파생 공식은 클라이언트·서버가 같은 코드를 import하는 @wr/analytics-core로 일원화했다. 인트라넷 전용·기본 비활성(STATS_WORKBENCH_ENABLED=false면 /api/stats/* 전체 404). 서버·DB·배포 구조(마이그레이션 6건, Python 엔진, 공유 패키지) 변경 폭이 커 major로 표기한다.
capabilities + user_capability_grants, 마이그레이션 0028) — stats.view·stats.regression·stats.export_results는 기본값 전원 허용(grant 불필요), stats.export_limited_rows(분석 응답의 이상치 원값·산점도 원시 점·회귀 관측치 진단값 노출)는 grant 필요, stats.export_phi는 admin+grant+step-up 예약(행 단위·PHI 내보내기는 후속 PR5로 미구현). 관리자 콘솔에 "통계 권한" 탭. 환자 완료시각 추적(0029) + 서버가 클라이언트 신고 없이 직접 재검증하는 server_verified_modules_complete_at(0030, completion.ts) — 환자 저장·워크스페이스 저장·영상분석 적용 경로 전부 배선.packages/analytics-core로 6개 평가 모듈의 계산·변수 추출 이관(결정적 마이그레이션·canonicalSerializer·coverage inventory 275개 필드 분류). 스냅샷 데이터셋 빌더(REPEATABLE READ·READ ONLY) + GET /api/stats/catalog·POST /api/stats/preview. 카탈로그는 통합 83개 변수(v28-age-at-injury+v1-snapshot-columns), grain 3종(case/job/disease), 공통변수 브로드캐스트, 롤업 변수(상병 any 판정·부위군·최장 근속 등), 필터 전용 변수, 어깨·경추·무릎 case 합계 변수.services/stats-engine/, 별도 venv — numpy 1.26.4·scipy 1.13.0·statsmodels 0.14.2·pandas 2.2.3), stats_runs(0031·0032), 4열 워크벤치 화면(카탈로그·레시피·결과·Inspector/리포트), 집계 결과 CSV 내보내기.*_EXPORT_NOT_SUPPORTED 400).patient.identity.ageAtInjury로 교체 · 상병 입력 진단명 잘림 수정 + 코드:이름 폭 1:3 · 등록 마법사 필수값 안내(이동은 허용) · 대시보드 의사명·나이 기준 수정.set-stats-available/open-statistics). 서버가 SPA를 서빙하므로 기존 클라이언트도 서버 업데이트만으로 헤더/랜딩 "통계분석" 버튼으로 접근 가능 — 새 설치본은 네이티브 메뉴 항목에만 필요하다.종합평가 패턴 그룹화 작업(#72)에서 K-L Grade를 조건부 표시로 바꾸며 넣은 화이트리스트가 실제 무릎관절염 상병 상당수를 걸러내던 문제를 수정. 입력창이 아예 렌더되지 않아 K-L Grade를 새로 입력하거나 고칠 수 없었다(기존 저장값 자체는 보존).
supportsKlGrade()가 M17.0/M17.9만 인정하던 것을 M17 하위코드 전체로 확장 — M17은 전 코드가 무릎관절증(gonarthrosis)이고 M17.1~M17.5(원발성·외상후·이차성 / 편측·양측)도 모두 K-L Grade 대상이다. 같은 파일의 모듈 자동 매핑(ICD_MODULE_MAP)은 이미 M17 전체를 무릎으로 잡고 있어 기준이 서로 어긋나 있었다.테스트 서버에서 손목(hand-wrist) 영상 분석이 GPU 없이 CPU로만 돌며 10분 job deadline을 초과해 실패하는 문제를 조사 — 실제 원인은 예전에 남아있던 도커 app 컨테이너(CPU 전용 pose-venv)가 포트 3001을 선점해 GPU 있는 네이티브 dev 서버가 EADDRINUSE로 조용히 죽어있던 것. 재발 방지 및 실패 원인 가시성 개선.
err.killed로 식별해 error_code를 뭉뚱그린 INFERENCE_ERROR 대신 DEADLINE_EXCEEDED로 명확히 표시(stderr가 ONNX 경고로 500자를 다 채워 실제 원인이 안 보이던 문제).dev-intranet-server.ps1 기동 전 포트(3001) 점검 추가 — 도커 app 컨테이너가 선점 중이면 자동으로 내리고 재확인, 그 외 알 수 없는 프로세스면 즉시 에러로 중단(과거엔 EADDRINUSE로 조용히 죽어 -NoExit 창만 몇 시간째 방치됨).dev-stack-down.ps1이 포트 소유 프로세스가 node.exe일 때만 강제 종료하도록 수정 — 그 외(도커 포워딩 프로세스 등)는 경고만 남기고 건너뜀(잘못하면 Docker Desktop 전체가 죽을 위험이 있었음).tsc --noEmit 클린, 실제 환경에서 포트 충돌 재현 후 수정 → 손목 분석이 inference_device_used=cuda로 정상 완료 확인.무릎/어깨(job-scope)·척추/경추(task-scope) candidate에만 있던 골격 검수·"왜 이 값?" 근거 패널이, 나머지 4개 모듈(무릎 비틀림·어깨/팔꿈치 반복·손목 반복/각도)의 candidate가 모이는 "flat 참고 후보" 리스트에는 배선돼 있지 않던 문제를 수정. evidence 데이터 자체는 서버 실분석 경로에 이미 있어(candidate도 evidenceByFeatureKey·fusion.candidates·segments를 받음) 렌더 배선만 추가.
FlatCandidateList 컴포넌트 추출)로 stub 호출 여부·인자를 직접 검증.repetitiveMediumHoursLeft/Right·repetitiveFastHoursLeft/Right 4종은 candidate value가 raw ratio라 근거 패널이 비율을 환산된 시간/일 값인 것처럼 오표시(예: "0.2 시간/일 = 자세비율 0.2 × 활동 200분/일 ÷ 60")하던 문제 — 근거 패널 전달 직전에만 candidateHoursPerDay로 보정.담당 아님(read-only)으로 열람 중인 환자 화면에서 데이터 편집뿐 아니라 화면 전환용 토글까지 통째로 막혀 있던 문제를 수정. StepContent의 capture-phase 차단(blockInteraction/blockMutatingKeys)에 이미 있던 data-readonly-allow 탈출구를 조회 전용 컨트롤들에 적용했다.
v6.4.0 이후 누적된 5건의 PR을 한 번에 반영.
canary/latest 채널. /updates/update-policy.json 관리자 on/off 스위치로만 체크가 활성화되고(파일 없음/파싱 실패/형식 오류는 전부 비활성 — fail-closed), 평상시엔 전 PC가 완전히 휴면. 종료/새로고침 draft 보호 뮤텍스에 설치 흐름 통합. Windows 네이티브 메뉴가 MenuItem 직접 변경으로는 갱신되지 않는 문제를 발견해 재빌드 방식으로 수정. 에어갭 export 스크립트에 SHA-512 기반 설치본/메타데이터 정합성 검증 게이트 추가. 코드서명은 후속 과제로 연기(보안 함의·승인 절차는 docs/INTRANET_DEPLOYMENT.md 7절). 로컬 풀스택 실측 + 실제 설치본 E2E(감지→다운로드→설치→재시작→재확인) 확인, 실제 병원 PAC·Win7/ia32·canary 실사용은 서버 배포 후 별도 진행.shared.reportOptions.assessmentOverride), 낡음(stale)/깨진 오버라이드 판정과 "자동 생성으로 되돌리기" 지원. 미저장 draft를 스텝/환자 전환·홈 이동·로그아웃·내보내기 6종 등 전 영역에서 가드(Electron은 IPC로 boolean만 동기화, 웹은 beforeunload). 선행 작업으로 종료/새로고침 시 네이티브 confirm 기반 exitFlow 상태 머신과 감사 로그 _source 분리(#90)를 먼저 배포.selectModuleNote()로 통합.updateModuleById 등이 메모이즈 시점의 오래된 권한 상태를 참조해 편집 가능/불가를 오판하던 문제 수정.일괄입력에서 생년월일 셀을 텍스트 서식으로 붙여넣어 4110-02-12 같은 오염값이 저장되면 사용자가 어떤 방법으로도 복구할 수 없던 문제와, 그 과정에서 겹쳐 있던 등록번호 재사용/삭제 관련 결함들을 함께 수정(#85). 별도로 환자 등록일이 KST 자정~오전 사이에 하루 전날로 표시되던 표시 버그도 수정(#86).
POST /api/patients/:id/identity-correction — 담당의/관리자 + If-Match + 락 게이트, 활성 case가 여럿이면 관리자 전용으로 전체 case의 birth_date/payload/revision을 함께 갱신.shared/contracts/patientDates.ts — 클라이언트·서버가 같은 규칙(실재 날짜·1900 이상·KST 기준 오늘 이하)으로 검증하고 canonical 형식으로 정규화, BatchImport 미리보기·POST/PATCH·workspace 저장 전 구간에 적용.withDeadlockRetry)을 실 PostgreSQL로 재현 후 수정.createdAt(UTC ISO) 문자열 앞 10자를 그대로 잘라 쓰던 사이드바 등록일 표시·정렬/필터·대시보드 최근 활동을, 공용 유틸 toLocalDateString으로 통일해 항상 로컬(KST) 캘린더 날짜로 표시하도록 수정.종합평가에서 패턴 그룹 선택 시 미리보기(및 EMR/엑셀 출력에 공유되는 formatGroupedAssessment)가 [상병 확인 · 업무관련성 높음] N개처럼 그룹명·구성원 수를 대괄호로 맨 앞에 표시하던 것을, 개별 종합소견과 같은 순서(상병 목록 → 상병 상태/업무관련성 → 낮음 사유)로 변경. 좌/우 표기는 상병 목록 줄에 이미 있어 상태 줄 앞에 중복 표시하지 않는다.
zod 3.x는 enum 키 z.record()라도 값 타입에 | undefined를 포함해 추론한다(런타임 파싱 결과가 모든 enum 키를 다 갖는다는 보장이 없어서 — 실제로 안전한 설계). squatDuration/overheadHours 같은 features 접근이 항상 존재를 가정하고 있어 tsc --noEmit에서 possibly undefined 오류가 나던 것을 두 테스트 파일에 non-null assertion을 추가해 해소. CI에 typecheck 게이트가 없어 그동안 방치돼 있었다.
Electron 앱을 X 버튼/메뉴 종료/Ctrl+Q로 닫아도 재실행 시 자동 로그인되던 문제와, 그 과정에서 발견된 서버 세션 동시성 버그들을 함께 수정.
rememberMe 플래그 전송(Electron=false, 웹=true 유지) → 서버가 Electron 세션은 리프레시 쿠키를 세션 쿠키(만료시각 없음)로 발급해 앱 재시작 시 자동 로그인되지 않게 함. 앱 시작 시 레거시(업데이트 이전) 영구 쿠키 정리. 정상 종료(X/메뉴/Ctrl+Q) 시 렌더러 경유 IPC 핸드셰이크로 서버 로그아웃 시도(single-flight, 3초 타임아웃 백스톱). logout()이 _retry:true로 401-refresh 인터셉터를 타지 않도록 함(서버가 refresh 쿠키만으로 revoke하므로 액세스 토큰 refresh 불필요). useAuthSync에 auth epoch 가드 추가 — 로그아웃 이후 뒤늦게 도착한 refresh 결과나 크로스탭 브로드캐스트가 세션을 되살리지 못하도록 방어.POST /api/auth/logout을 refresh 쿠키 기반 인증으로 재작성(auth/csrf 미들웨어 제거) — 액세스 토큰 없이도 동작, 만료된 세션도 즉시 revoke 가능. sessions.family_id 도입 + rotateSession/revokeSessionFamily 모두 family 단위 advisory lock(pg_advisory_xact_lock) 적용 — 동시 refresh 두 개가 같은 토큰으로 경합하거나, logout이 예전 세대 토큰을 든 채 동시 refresh가 새 세대 세션을 만드는 경우에도 세션이 살아남지 않도록 직렬화. CSRF 검증을 revokeSessionFamily 트랜잭션 안으로 이동 — 별도 사전조회의 grace window 불일치로 오래된 토큰이 CSRF 검증 없이 family를 revoke할 수 있던 구멍을 닫음.sessionStore.integration.test.ts) 신규 — 두 경쟁 조건(같은 토큰 동시 rotation, 이전/현재 세대 간 logout↔refresh 경합) 모두 수정 전 상태로 되돌려 실패 재현 후 수정 복원해 통과까지 확인. 서버/루트 typecheck 클린, lint 0 errors.동일 계정 다중 클라이언트 동시 편집 시 "저장 안 됨" 사고에 대응하는, 서버가 실제로 강제하는 환자 단위 편집 락. 기존 revision 기반 optimistic lock은 최종 안전망으로 그대로 유지한다.
patient_locks 테이블(migration 0024) — lease_token_hash로만 소유권 판정, client_instance_id 자기매칭으로 F5 복구 지원, TTL 만료로 스윕 잡 없이 자연 정리. server/src/db/patientLocks.ts — acquire/renew/force/peek/release + 쓰기 게이트(checkLockForWrite), patient_records FOR UPDATE 앵커로 락 관련 트랜잭션 전체를 직렬화. patients.ts에 POST/DELETE/GET /:id/lock 신설, patch/delete/assignPatient에 앵커+게이팅 배선, 소프트 삭제 시 락 행 정리. videoAnalysis.ts applyJob에도 동일 게이팅. LOCK_ENFORCEMENT_MODE(off/observe/enforce) 3단계 롤아웃 플래그 — off는 기존 동작과 완전 동일.usePatientLock(신규) — 환자를 열면 즉시 acquire 시도, ttlMs/4 renew 하트비트, force-takeover, 세대번호로 stale 응답 방지, 창 포커스/탭 활성화/30초 주기 안전망 + 지수 백오프(30초~5분)로 'lost'/'held-by-other' 상태 재시도. usePatientSync가 락 상태로 활성 환자 push를 게이팅, flushPatient는 기존 autosync 큐에 합류. useSyncStatusSummary(신규) — 충돌/락상실/저장대기/오프라인 통합 배너용. App.jsx에 락↔싱크 브리지, 세션 identity 변화 시 토큰 초기화, held-by-other/lost 배너 + 강제 획득 버튼.clock_timestamp() 미사용(TTL이 트랜잭션 시작 시각에 고정), observe 모드 감사로그 누락, 만료 경계에서 peekLock()의 null을 LockRow로 위장하던 버그, syncPaused 재진입/local-only 환자 해제 누락, <StrictMode> 중복 acquire, 영상 분석 apply 누락 토큰, 프리셋 모달의 락 가드 우회.TEST_DATABASE_URL 있을 때만 실행).syncCervicalModuleData/syncElbowModuleData/syncWristModuleData가 JSON.stringify로 변경 여부를 비교하는데, 정규화 과정에서 객체 키가 고정 순서로 재정렬되면서 원본과 키 순서만 달라도 changed=true로 오판했다. PostgreSQL JSONB는 키 삽입 순서를 보존하지 않아 서버 왕복 시마다 이 오판이 반복되어 동일 payload가 계속 재저장되는 무한 루프가 발생했다.
stableStringify()를 core/utils/common.js에 추가하고 세 모듈의 serialize()에 적용. 회귀 테스트 추가.경추 상병 평가(상병 상태·업무관련성)를 다 채워도 부담 작업을 하나도 입력하지 않으면(= "경추부담 작업 없음", 유효한 상태) 환자 목록 사이드바가 영원히 미완료(빨간 점)로 남던 문제 수정.
isCervicalAssessmentComplete()(사이드바 완료 배지 판정)가 화면 표시 로직과 별개로 자체 완료 기준을 갖고 있었는데, 모든 직업의 작업 수가 0건이면 무조건 false를 반환하는 조기 반환문이 있었음 — 부담 작업이 실제로 없는 정상 케이스와 "작업 입력을 깜빡한" 케이스를 구분하지 못했음.cervical/utils/calculations.js, 신규 cervical/utils/__tests__/calculations.test.js(회귀 테스트 7건).npm run lint 0 errors, build:web 통과. Playwright로 실제 환자(경추 상병만, 작업 0건, 상병 평가만 입력) 생성해 사이드바 완료(초록 점) 표시 확인.종합평가 패턴 그룹화(v6.2.0) 도입 후 실사용 피드백을 반영해 미리보기 탭 구성과 그룹 카드 표시를 다듬음.
generateUnifiedEMR(patient, groupOutputOverride)에 override 파라미터를 추가해 저장된 reportOptions 값과 무관하게 그룹/개별 두 형식을 동시에 계산.[직업명])를 추가하고 직업이 바뀔 때마다 줄바꿈.AssessmentGroupView.jsx, AssessmentStep.jsx, emrReport.js(generateUnifiedEMR override + buildExposureSummary 직업명 헤더).npm run lint 0 errors, build:web/build:electron 통과. Electron/서버 무변경.동일 BK유형으로 묶인 상병 카드가 대표값만 화면에 보여주고 개별 진단의 실제 저장값은 비어있는 채로 남아, 입력 순서(신규 상병 자동 합류 / 개별 카드 첫 BK유형 수동 선택 / 진단코드 변경에 의한 자동추론 변경)에 따라 "다 채웠는데 미완료" 플래그가 뜨던 문제 수정.
bkAutoSyncedFrom 마커, 사용자가 직접 편집하면 자동 해제)..find())에서 화면 표시(pickRepresentativeEntry)와 동일한 최고 점수 기준으로 통일.npm run lint 0 errors, npm run build:web 통과, 클라이언트 953 tests pass.상병이 50건을 넘어가면 종합소견 좌측 패널에서 우/좌 개별 클릭이 과도해지고, EMR 종합소견(txtSyth1Cont)이 CP949 3,950byte를 넘어 잘리던 문제를 해결.
AssessmentGroupView/AssessmentPatternEditor 신규. 기존 환자는 shared.reportOptions.groupAssessmentResults 옵션 미설정 시 출력이 그대로 유지되고, 그룹 뷰 자체는 이 옵션과 무관하게 항상 열려 있어 기존 환자의 미완료 건도 그룹 화면에서 바로 처리 가능.emrText.js로 CP949 byte 계산/절단 유틸 분리 + b6 신체부담 평가 요약본(buildExposureSummary) 추가로 b8 축소, 미리보기에 byte 게이지 탭 추가. EMR 전송 전 종합소견이 한도를 초과하면 확인창 표시.supportsKlGrade/supportsEllmanClass — 무릎관절증(M17.0/M17.9)·회전근개(M75.1) 등 조건에 맞는 상병에만 K-L Grade/Ellman Class 입력창 노출(무관한 상병에 불필요한 입력란이 뜨던 문제 해소).GET /api/patients?scope=<의사UUID>를 프론트에서 노출. admin이 특정 의사·미배정을 선택해도 무조건 all로 되돌아가던 기존 버그 수정. 명부 로딩 상태(loading/ready/error)를 도입해 사이드바·대시보드 두 곳의 scope 유효성 가드를 하나로 통합(정책 충돌 제거).inert 속성으로 전체 콘텐츠를 감싸던 방식이 텍스트 선택(복사)과 스크롤까지 막아 내용이 길면 읽을 수조차 없던 문제 수정 — capture-phase 이벤트 차단(클릭/키보드/입력/붙여넣기/드롭)으로 교체해 편집 상호작용만 막고 복사·스크롤은 허용. 프리셋 적용 경로에 누락돼 있던 담당의 권한 체크도 함께 보강.tryRegister()가 5분 flush 주기 그대로 재등록 확인을 보내 시간당 12회로 서버 한도(5회/시간/사용자)를 항상 초과했고, 429 응답을 하드 실패로 오인해 상태를 error로 강등 — 관리자가 승인해도 클라이언트가 "장치 등록 필요"를 계속 표시하던 문제. 재시도 간격을 20분으로 늘리고 429는 상태를 유지한 채 조용히 재시도하도록 수정.@testing-library/react/user-event 신규 도입(이 저장소 첫 컴포넌트 렌더링 테스트). 관련 로직 단위/렌더 테스트 36건 추가.build:web 통과. 서버/Electron 무변경 → 인트라넷 설치본 재배포 불필요(서버 이미지만 갱신하면 반영).repetitiveMediumHours/repetitiveFastHours(상완거상 반복 4repetitiveMedium/FastHoursLeft/Right) 신규. 어깨 반복 계열 6키 전부 "상지반복" 프로필에서만 요청·생성·표시(신규 videoFeatureProfiles.js가 프로필 정책 단일 source).processFeatureAggregationMode 계약 필드 신규 — 리로드 후에도 공정별 기여값 재계산이 정확하도록 분석 소스(mock/서버)를 영속화.build:web/lint 통과. dev 라이브(Playwright)로 공정별 기여값 합=직업 합계 수치 정합 확인. Electron 셸 무변경 → 인트라넷 설치본 재배포 불필요(서버 이미지만 갱신, 오프라인 패키지의 electron/ 폴더는 v6.1.4 인스톨러 재사용). 상세는 docs/VIDEO_ANALYSIS_IMPLEMENTATION_PLAN.md 6.0-16/6.0-17 항목.SSRHPLANLIST_DblClick(iCol, iRow)=(열, 행)·행 1-based인데 v6.1.3 리팩터에서 (row, 1)로 호출해 항상 1번 행만 로드되던 문제 → (0, row) + 1-based 스캔으로 복구. 사용자가 입력한 재해일자는 보존(미입력 시에만 자동 채움)하여 불일치 폴백이 입력값을 덮어쓰지 않음.exportBatchTemplate)을 받을 수 있음 — 처음 사용자가 작성용 엑셀을 만들 수 있게.SSRHPLANLIST)에 재해일자가 다른 여러 신청건이 있으면 환자 재해일자와 일치하는 건만 로드. 미입력 시 1번 항목 + "재해일자가 다른 신청건이 있습니다" 경고, 일치 건 없으면 1번 항목 + 경고. 배치 모드 stale-grid 방지 witness(txtSsnNo) + 변경감지 폴링.txtSex(남/여) 우선, 비어 있으면 주민번호 7번째 숫자(1·3·5·7=남, 2·4·6·8=여)로 파생 → 앱 male/female 매핑.직업 노출 프리셋을 개인(private) 저장에서 조직 공유(organization) 로 확장. 저장 모달 공개범위 토글(인트라넷 기본=조직 공유) + 검색/조회 소유자 배지·읽기전용 + 관리자 '프리셋 공유' 탭(양방향 전환) + 마이그레이션 0023(기존 private 백필, 토글 UI와 동일 릴리스로만 배포). Electron 셸 무변경 → 서버 이미지만 갱신.
손목(wholebody) 추론 처리시간 deadline 일관화(클라 폴링이 추론보다 먼저 포기하던 문제 해결) + 조직별 추론 디바이스 토글(auto/cpu/cuda) + 검수 overlay 손 keypoint 가독성 개선. 마이그레이션 0022. Electron 셸 무변경. (상세: docs/VIDEO_ANALYSIS_IMPLEMENTATION_PLAN.md)
근골격계 부담작업 영상분석에 손목/손가락(wholebody 포즈)·상지 반복빈도를 "참고용 candidate"(자동입력 없음)로 추가. 손목은 "손목·손" 프로필 클립만 on-demand wholebody 추론(굴곡=측면·편위=정면 시점 게이트), 어깨/팔꿈치 반복빈도(cycles/min) candidate. 에어갭 모델 manifest role 분리(pose-body/pose-wholebody). (M4 6.0-10·6.0-11 — 상세는 docs/VIDEO_ANALYSIS_IMPLEMENTATION_PLAN.md)
근골격계 부담작업 영상분석(자세 추정)을 "참고용 시범 운영"(미검증 배너 + 수정사유 피드백, 자동 게이팅 비활성)으로 활성화하고 에어갭 오프라인 배포 패키지를 완성.
.onnx SHA256 대조). 상세는 docs/VIDEO_ANALYSIS_IMPLEMENTATION_PLAN.md.scripts/export-offline-package.ps1 — 실파일 sha 검증·dirty 가드·release-manifest.json 생성. compose에 video_uploads 볼륨 + 추론 mem/cpu 제한.--network none 추론 smoke 통과.전체 코드 리뷰(보안/리팩터링/정리) 1차 적용분. 즉시 적용 가능한 보안 보강과 정리 작업.
api/analyze.js): 허용된 Gemini/Claude 모델 외 요청은 400 반환 — 비용 탈취·경로 조작 방지.electron/main.js): fs-migrate 핸들러 3곳에 sanitizeId() 적용(path traversal 방지), set-access-token에 isAllowedSender origin 검사 추가.escapeHtml 적용 (knee는 기존부터 적용됨).electron/preload.js, types/placeholder.d.ts, artifacts/elbow_module_structure.md 삭제 + CLAUDE.md/AGENTS.md/README.md의 구조 참조를 preload-intranet.js/preload-standalone.js로 갱신.npm run build:web 통과. package.json 버전 미변경.handleImport를 knee/shoulder/elbow/wrist/
cervical/spine 6개 모듈의 registerModule().batchImportConfig(columns/applyRow)로
분리, 공통 헬퍼는 src/core/utils/batchImportHelpers.js로 이동.useAuthSync,
useAppSettings, useEvaluationDateSync, useElectronMenuEvents,
useConflictResolution) + AppModals.jsx 컴포넌트로 순수 이동(로직 변경 없음).ai-models.config.cjs 신설
(ALLOWED_MODELS / DEFAULT_CLAUDE_MODEL / DEFAULT_GEMINI_MODEL / CLAUDE_MAX_TOKENS /
GEMINI_MAX_OUTPUT_TOKENS) — api/analyze.js(ESM)와 electron/main.js(CJS) 양쪽의
하드코딩 제거 후 공용 참조.src/core/utils/data.js(859줄)의 테스트 데이터
생성 로직을 src/core/fixtures/createTestPatients.js로 이동, usePatientCrud.js에서
동적 import로 로드 — 빌드 시 별도 청크(createTestPatients-*.js)로 분리.BATCH_HEADERS/
generateBatchRows)의 'Ellman(좌)' 컬럼 뒤에 상병상태(우/좌)·업무관련성(우/좌)·
업무관련성낮음사유(우/좌)·수직분포원리·동반척추증 8개 컬럼 추가. 재import 시
batchImportHelpers.js의 applyDiagnosisAssessment가 값을 진단에 반영(spine
전용 필드는 moduleId === 'spine'일 때만). 기존 환자·기존 진단의 평가값만 갱신되는
재import도 "가져올 데이터가 없습니다"로 막히지 않도록 stats.updatedAssessments
카운터를 완료 조건에 추가했고, 기존 값과 동일한 평가값은 갱신 건수에서 제외.docs/VERCEL_AI_PROXY_HARDENING.md 신규(운영자용 Vercel 보안 설정 가이드).npm run build:web / build:electron 통과, npm run lint 0 errors.v5.1.3 공식 환자의 일일선량(dailyDose) 임계치가 너무 높다는 임상 피드백 반영. 임계치는 평생누적용량 포함/제외 게이트이자 중증도 "중등도하" 진입값이라, 사다리 전체를 함께 하향.
thresholds.dailyDose.v513): 남 5.5 / 여 3.5 → 남 4.0 / 여 3.0 kN·h (legacy 남 2.0 / 여 0.5 불변).classifySpineSeverity): 정연한 비례(여=남×0.75)로 재설계 — 남 중등도하 ≥4.0 / 중등도상 >6.0 / 고도 >8.0, 여 ≥3.0 / >4.5 / >6.0. 압박력(N) 경계(6,000/5,000/4,000N)와 단일 사다리 "모든 환자 일괄 적용" 구조는 불변.thresholds.js, calculations.js(classifySpineSeverity), 테스트(경계값 가드 보강), docs/PRD.md. 테스트 446개 통과.척추 모듈에 전신진동(BK2110) 평가를 추가하고, 기존 요추 압박력(MDDM)과 한 환자에서 독립적으로 공존하도록 구조 개편.
전신진동(BK2110) 신규:
A(8)=aw·√(T/8h), 직업 내 다구간 에너지합, 평생 DV=Σ Amax(8)²·근무일수·근속연수(Amax(8)≥0.63 게이트).Amax(8)≥0.63 m/s², 평생 DV,RI=1400 (m/s²)². 다중 직업은 Amax(8) 직업별 최대 집계 + DV 직업 간 합산. 위험도는 평생 DV 기준.vibrationCalc.js(계산), VibrationEvaluation.jsx·VibrationIntervalManager/Editor·VibrationResultPanel(UI), time.js(순환참조 회피용 leaf), sectionText.js(WBV 섹션 텍스트), 테스트 vibrationCalc.test.js.MDDM ↔ WBV 공존 (구조 개편):
computeSpineCalc가 MDDM(top-level 평탄 필드) + WBV(calc.vibration 서브객체)를 함께 반환. 기존 evalMethod 디스패처(상호배타) 폐기 → 종합소견·EMR·엑셀에 두 평가 모두 출력.activeSpineTab)으로 MDDM/WBV 편집 전환, 결과 패널은 둘 다 렌더(각자 status로 게이트).(MDDM 유효 || WBV 유효) && 상병 — 둘 중 하나만 평가해도 완료.resolveMddmStatus/resolveVibrationStatus 헬퍼로 기존 환자(MDDM 작업 있으면 present)·1차 WBV 환자(intervals 있으면 present) 마이그레이션. 일괄 엑셀은 MDDM present일 때만 작업 행 생성.patientCompletion이 isComplete에 activeModules 전달(진단 모듈 매핑 fallback 보강). AI 시스템 프롬프트에 BK2110 기준 추가. cervical generateJobNarrative 미사용 인자 제거(lint).테스트 442개 통과, build:web 성공.
v5.1.3 일일선량 공식 정정으로 dailyDoseKNh 자릿수와 분포가 바뀐 뒤에도 임계치·중증도·위험/업무관련성 분기는 옛 공식 기준 그대로였음. 새 공식 스케일에 맞춰 일괄 재조정하고, KPI/위험/업무관련성이 비교 대상으로 삼는 기준을 독일 법원(BSG) 단일로 통일.
척추(spine):
thresholds.dailyDose 버전별 분기 — v5.1.3: 남 5.5 / 여 3.5 kN·h, legacy: 남 2.0 / 여 0.5 kN·h (보존). calculateLifetimeDose(..., formulaVersion)이 환자 버전에 따라 임계치 선택.classifySpineSeverity 새 공식 스케일로 재조정 — 남 고도 >10 kN·h, 중등도상 >8.0, 중등도하 ≥5.5; 여 고도 >8.0, 중등도상 >5.5, 중등도하 ≥3.5 (최대압박력 기준은 남녀 모두 6000/5000/4000N으로 통일).독일 법원(BSG) NN%, highlight = court.percent ≥ 80. 하단 3개 비교 카드(MDDM/독일 법원/DWS2)는 참고용으로 그대로 유지.assessRisk): comparison.court.percent 직접 판정. >100% danger / 80~100% warning / <80% safe.assessWorkRelatedness): court 단일 3단계. >courtLimit 높음 / courtHalf≤x≤courtLimit 불충분(다른 요건 고려) / <courtHalf 낮음. 기여도 분모 = courtLimit.[적용 공식: …] 라인 제거 — 외부로 나가는 산출물에는 비공개. SpineResultPanel 화면 배지는 임상가 확인용으로 유지.경추(cervical) 모듈:
riskFactorItems(BK2109 한정 4개) → flagItems(확인된 모든 양성 flag)로 확장. 부담평가/종합소견 미리보기·EMR·통합 Excel 모두 동일.generateJobNarrative 첫 줄 직업: {jobName} 제거 → 카드 제목/- 직력N: 라인의 직업명 중복 해소.기타:
patients.length === 0일 때만 "환자 목록 보기" 노출(서버에 환자만 있는 케이스의 진입 경로 유지).영향 범위: 환자 데이터 스키마 변경 없음. legacy 환자도 spine 작업을 편집하면 기존 promoteSpineFormula로 자동 v5.1.3 승격 + 새 임계치 적용(공식·임계치 묶음 일관). 397 tests pass.
v5.1.3에서 환자별로 legacy / v5.1.3 공식이 분기되지만 UI에 표시되지 않아 임상가가 어느 공식 적용 중인지 알 수 없던 문제 해결.
MDDM v5.1.3 (초록) / MDDM 레거시 (호박색) 배지 추가, 마우스 오버 시 의미 + 자동 승격 안내 tooltip[적용 공식: ...] 라인 추가 — 정적 산출물(소견서/내보내기 텍스트)에도 어느 공식으로 계산된 결과인지 기록되어 사후 추적·재발급 시점 비교 가능.spine-formula-badge 스타일배포 시 같은 v5.1.3 태그 덮어쓰기 회피를 위해 5.1.4로 patch 범프. 계산 로직 변경 없음, 기존 환자 데이터 영향 없음.
공식 정정: calculateDailyDose를 원형 MDDM 공식 D_r = √(Σ F²·t / 8h) · 8h로 정정 (8h 정규화/재곱 누락, 시간을 초로 누적하던 단위 불일치 해결). 동일 입력에 대해 새 공식 결과가 이전 대비 약 ×2.83(=√8) 증가.
레거시 결과 보존: 모듈 데이터에 formulaVersion 필드 신설. 기존 환자(필드 부재)는 옛 공식 그대로 — 이전 PDF/EMR 출력과 100% 동일하게 일일선량·평생누적량·위험도·작업별 일일 기여 모두 보존. spine 작업을 실제로 추가/수정/삭제·드래그 reorder·프리셋 적용·BatchImport로 task 생성한 시점에 자동으로 v5.1.3으로 승격되어 새 공식 적용. sharedJobId 자동 정리(단순 열기) 경로에서는 승격 안 함. 서버 마이그레이션 불필요(JSONB payload에 자연 흡수).
여성 중증도 경계값 분리: 종합소견의 일일 노출 중증도(고도/중등도상/중등도하/경도) 기준이 기존 남녀 공통 → 남녀 분리. 남성은 기존 동일, 여성은 더 민감한 기준(0.5/2.0/3.0 kN·h, 3,000/4,000/5,000 N).
작업별 일일 기여 표시: v5.1.3 환자에서 작업별 기여는 총량을 F²·t 비중대로 배분 → 작업별 합 = 총 일일선량 보장(비가산성 해결). legacy 환자는 기존 단일 작업 공식 그대로 유지.
코드 정리: reportGenerator.js·exportService.js에 중복되던 spine 중증도 분류와 작업별 기여 계산을 src/modules/spine/utils/calculations.js의 classifySpineSeverity·getSpineTaskDoses로 추출. 상수는 src/modules/spine/utils/formulaVersion.js에 분리.
서버 모드 전환 후 노출된 최근활동 회귀 (다음 날 옛 환자가 다시 상단에 올라오는 증상)를 데이터 계약 차원에서 수정. 대시보드 카드 의미 강화 + 인트라넷 테스트 버튼 가드.
최근활동 timestamp 계약
toResponse(): updatedAt(DB updated_at으로 무조건 덮어쓰기, stale payload 차단) + createdAt(payload 값 우선, 없으면 DB created_at) 명시getRecentActivityTimestamp: updatedAt → _savedAt → createdAt 폴백, sync.lastSyncedAt은 동기화 시각이라 의도적으로 제외, Date.parse() 숫자 비교touchPatientRecord가 모든 환자 변경 진입점에서 updatedAt 일관 set (이전엔 caller별로 비대칭)대시보드 카드 확장
jobs[0].jobName 기준)diagnoses[].code 기준, 한 환자 중복 카운트 없음)normalizeGender(M/F·남/여·male/female 정규화), computeAge(formatBirthDate 재사용으로 YYYYMMDD도 처리)repeat(auto-fit, minmax(200px, 1fr)) + grid-auto-rows: minmax(170px, 1fr) — 카드 수 증가에 자동 적응, 모든 카드 동일 높이테스트 버튼 가드 (인트라넷)
showConfirm으로 "목록 교체 + 서버 동기화 가능성" 안내자동 매핑이 실패한 진단을 모듈에 수동으로 지정할 수 있도록 정책과 UI 통합.
진단 모델 + 매핑 정책
moduleId 필드 추가 (null=자동, 'knee'/'spine'/...=수동 지정, '__none__'=해당 없음)resolveDiagnosisModule() 우선순위 단일화: __none__ → 수동 → 자동 키워드 hint → 단일 활성 모듈 fallbackisCervicalDiagnosis, isElbowDiagnosis, isWristDiagnosis, knee/spine/shoulder 인라인 필터)를 resolveDiagnosisModule 기반으로 통일 — 자동 매핑이 실패한 진단도 수동 지정만 하면 해당 모듈 화면에 즉시 노출MODULE_LABELS export + isValidDiagnosisModuleId() 헬퍼로 UI/resolve 유효성 기준 일원화UI
· 수동 표시moduleId를 selectedModules에 자동 병합 → 모듈 선택 단계에서 빼먹어도 진단에 지정된 모듈은 자동 활성화updateDiagnoses가 수동 지정 모듈을 activeModules에 자동 추가 (기존 modules[id] 데이터는 보존)자동 매핑 키워드 보강
족관절|발목 → knee 모듈로 임시 흡수 (전용 모듈 추가 시 분리 예정)척골 → wrist 모듈인트라넷 Workspace Autosave 비활성화
workspaceAutosavePolicy 헬퍼 신설, useWorkspacePersistence + workspaceRepository 인트라넷 가드 추가v5.0.0 인트라넷 백엔드 도입 후 실제 운영에서 드러난 권한 미비점과 UX 결함을 정리.
환자 권한 정책 (서버 + UI)
assigned_doctor_user_id) 또는 admin만 가능. 다른 의사 환자는 조회만 허용assignedDoctorOrAdmin — PATCH/DELETE /api/patients/:id에 적용. 다른 org는 404(존재 누설 방지), 비담당은 403POST /:id/assignment(인계)는 현행 admin 전용 유지assignedDoctorUserId 자동 mirroring → sync 전에도 본인 환자 정상 수정canEditPatient / canDeletePatient — 로컬 모드는 단일 사용자라 항상 true, redacted는 항상 falseinert 속성으로 감싸 키보드 포커스/탭/스크린리더까지 차단usePatientCrud.updatePatient silent guard (EMR import / preset select / conflict resolve 등 우회 경로 차단)대시보드 scope 분리 (내 환자 / 전체)
다중 사용자 운영 UX
척추 모듈 개선
Caddy 호스트 포트 변경
CORS_ORIGINS 환경변수에 :8443 포함 필요 (.env.production, docker-compose.yml 반영)개발 환경
docker-compose.override.yml — dev 스택만 http://localhost:3000 (Vite) CORS 허용. prod compose는 영향 없음신규 문서
docs/UPDATE_5.1.0.md — v5.0.x → v5.1.0 현장 업데이트 절차 (예상 다운타임 ~10초, 검증 시나리오 8개, 롤백 무손실)검증: 클라이언트 299 + 서버 369 = 668 tests pass. lint 0 errors. 빌드 성공.
병원 인트라넷 환경에서 다중 사용자 운영을 위한 풀스택 백엔드를 도입했다. 기존 standalone(Electron/웹) 모드는 그대로 유지하며, 빌드 타깃으로 인트라넷 모드를 분리.
백엔드 인프라 (신규)
wr_audit_reader)!reset 태그로 prod 오버레이 (포트 미노출, healthcheck)Electron 인트라넷 빌드 (신규)
electron/build-target.json으로 standalone/intranet 분기 (preload 분리)flushQueue (5분 간격)에서 pending → active 자동 인식다중 사용자 시스템 (신규)
데이터 동기화 (인트라넷)
오프라인 배포 패키징 (신규)
.env, .asc private key 등 패키지 제외 확인)docker load 일괄 처리T46 프로덕션 릴리즈 리허설 (전 섹션 PASS)
wr-prod_*), migration 정합성, admin seed (비대화형 파이프 입력), 백업 + 복호화 검증, 롤백 dry-run, Go/No-Go 판정backup.sh의 write_json_atomic에서 _alerts/*.json 생성 후 chown 1000:1000 적용 → admin 콘솔의 "해결" 버튼 500 에러 수정restore.sh에 GPG_PASSPHRASE 환경변수 지원 추가 + 복구 전용 키페어(passphrase 없음) 별도 발급 가이드 추가문서 (신규/대폭 개정)
docs/OFFLINE_DEPLOYMENT_PACKAGE.md — 단계별 설치 절차 (Windows/Linux 분리, 12개 섹션, PowerShell 실행 정책 가이드 포함)docs/PRODUCTION_RELEASE_PLAN.md — 운영 절차서 (롤백 경로 6-2/6-3 분기)docs/T46_GO_NO_GO.md — 리허설 결과표docs/T46_IMPLEMENTATION_PLAN.md — 리허설 구현 계획docs/OPERATIONS_RUNBOOK.md — 운영 런북docs/BACKUP_MONITORING_PLAN.md — 백업 모니터링 설계docs/INTRANET_DEPLOYMENT.md — HTTPS / 내부 CA / 인증서 신뢰 등록 (3가지 방법)docs/BACKUP_RESTORE.md — 백업·복구 상세대시보드 개선
dashboardStats.js에 세분화된 계산 로직 추가환자 목록 & 검색 강화
JobFilterCombobox 컴포넌트로 직업명 검색 제안스타일 & CSS 변수
.dashboard-chart-area, .stacked-bar, 범례 스타일 추가.advanced-filter-section, .filter-group 스타일 추가.jobfilter-combobox-list, .suggested-item 스타일코드 개선
App.jsx 대규모 분리 리팩터링
src/core/components/)
LandingScreen: 홈 화면 및 환자 목록IntakeWizard: 신규 환자 3단계 위자드 (기본정보 → 상병 → 모듈 선택)PatientSidebar: 환자 목록 사이드바 (검색/필터/정렬)MainHeader: 상단 헤더 (환자 정보, 설정, 도구)StepContent: 활성 스텝 콘텐츠 렌더러StepIndicator: 위자드 진행 표시기SaveLoadModals: 저장/로드 모달src/core/hooks/)
useEMRIntegration: EMR 통합 상태 & 동기화useExportHandlers: 내보내기 (Excel/PDF) 로직useIntakeWizard: 신규 환자 위자드 상태usePatientCrud: 환자 CRUD 작업usePresetManagement: 프리셋 로드/저장/캐싱useStepNavigation: 스텝 네비게이션useWorkspacePersistence: 워크스페이스 자동 저장/복구src/core/utils/)
steps.js: buildSteps() — 활성 모듈에 따른 동적 스텝 생성경추 모듈 계산 로직 최적화
RISK_FACTOR_FLAGS를 warning tone 4개(하중/운반시간/강제목자세/누적하중)로 한정 — 진단 지지 플래그(positive/info)가 "업무관련성 위험 요인" 란에 혼입되던 문제 해결mechanical_cervical_load_dominant를 FLAG_ORDER에서 제외하고 종합 배지로만 표시 — 위험요인 카운트 중복 방지syncCervicalModuleData 이중 호출 → computeCervicalCalc 단일 호출로 통합syncCervicalModuleData 및 SpineEvaluation useEffect에서 자동 제거; jobs 배열이 임시로 비는 경우 pruning 건너뜀(shouldPrune 가드)sharedJobId가 비어 있는 초기 기본 task를 applyToModule에서 교체 대상으로 처리 — 프리셋 적용 후 "작업 1"이 잔존하던 문제 해결applyToModule의 불필요한 id: createCervicalTask(...).id 재할당 삭제tasks[] (sharedJobId로 직업 연결 — 척추 패턴과 동일)presetConfig — 공통 노출 7개 필드 추출/적용)<경추(목)> 섹션 자동 포함getPresetCategory/getPresetDescription null 안전 처리 — 프리셋 저장 버튼 클릭 시 빈 화면 크래시 수정EmrHelper.cs에 진료기록분석지(--extract-record) / 다학제회신(--extract-consultation) 추출 모드 추가. IE COM 자동화로 환자명, 생년월일, 재해일자, 진료기록, 기저질환, 수진이력, 상병 목록, 과별 회신 자동 읽기다학제 추출 버튼으로 과별 회신 읽기 → 다학제 보내기 버튼으로 EMR 종합소견 2,3번 칸에 입력patientNo): 기본정보 입력, 대시보드 테이블, 일괄 Import에 등록번호 컬럼 추가txtMrecMedPovCont) 직접입력 지원, 다학제 회신 요약을 종합소견 엑셀에 포함_pendingPreset 대기 메커니즘runHelper() 함수로 EMR 헬퍼 실행 경로/타임아웃/에러 처리 통합PresetManageModal — 현재 입력된 신체부담 데이터를 프리셋으로 저장. 모듈별 선택, 데이터 미리보기, 커스텀 프리셋 삭제 지원presetRepository.js — builtin(job-presets.json) + custom(localStorage/Electron FS) 이중 저장소, 병합 로드, JSON 내보내기/가져오기extractFromModule/applyToModule 계약으로 프리셋 시스템 연동src/modules/elbow/ — BK2101/2103/2105/2106 4유형 분기, Gate-and-Flag 판정, 공통 시간적 선후관계(모듈 전체 1회 입력), 직업 × 상병 2차원 jobEvaluations[] 구조bkSelectionMode: auto | manual) 지원work_pattern 수식자: continuous 시 daily_share 임계값 상향(1.5h/20% vs 3h/40%), rest_unfavorable이 moderate 휴식에서도 활성화excelSingle) + html2pdf 기반 PDFgenElbowBurdenSection 추가(< 팔꿈치(주관절) > 섹션). 척추 섹션은 helper 함수로 리팩터링되어 DWS2/법원/MDDM tiered 해석 문장 자동 생성returnConsiderations를 공유하도록 fallback 확장dynamic 키워드 제거, [STAThread] 추가, IHTMLDocument2 단일 시도로 lResult 소모 문제 해결, --diagnose 모드 추가x-goog-api-key 헤더로 전환, CORS 오리진 허용 목록 적용(* 제거), IPC 경로 순회 방어(sanitizeId), netRequest() HTTP 상태코드 검사 추가handleStartIntake stale closure 수정(useRef 패턴), 모듈 스텝 인덱스 동적 계산(하드코딩 3 제거), 단일 환자 삭제 시 확인 대화상자Date.now() → crypto.randomUUID()로 변경, localStorage QuotaExceededError 처리(safeSetItem 래퍼)createdAt(등록) / updatedAt(마지막 수정) / evaluationDate(평가 완료) 명확히 구분등록일/접수일 열 인식 (37열){userData}/wr-eval-data/) → localStorage 5-10MB 제한 극복, 수천 명 이상 관리 가능. 웹은 기존 localStorage 유지require('../package.json') asar 경로 버그 수정 → 모든 IPC 기능 정상화상세 PRD는 docs/PRD.md를 참고하세요.
X-WR-Client, 필터 용도) → Upstash 속도 제한(IP 분당 10·일당 100 + 전역 일일 예산 WEB_AI_DAILY_BUDGET, 호출 횟수 기준) → 구조화 입력 → 모델 허용 목록 순으로 검사하고, 설정이 빠지면 열어 두지 않고 503으로 닫는다(fail-closed). Redis 시간 초과도 장애로 취급한다. Electron 독립형은 모델명 허용 목록·60초 제한·1MB 응답 상한·오류 비노출을 적용한다. Vercel이 함수를 CommonJS로 변환하므로 계약은 .cjs 빌드로 가져온다(미리보기에서 ERR_REQUIRE_ESM으로 함수가 로드되지 않던 문제를 재현·수정, 회귀 테스트 포함)./updates/에 쓸 수 있는 쪽이 설치본과 yml을 함께 바꿔치기하면 통과했다. <channel>.yml에 Ed25519 서명(<channel>.yml.sig) 을 붙이고, 셸이 내장 공개 키로 서명을 확인한 뒤에만 설치본을 내려받는다.
electron-updater가 받은 정보와 깊은 비교 순서로, 서명되지 않은 입력은 YAML 파서에 넣지 않는다. autoDownload=false로 바꾸고 서명 게이트가 downloadUpdate()를 부르는 유일한 경로다. 서명 없음·다른 키·바꿔치기·정보 불일치·공개 키 없음·검증 중 오류는 모두 다운로드하지 않는다(fail-closed). 거부는 메뉴에 "업데이트 서명 검증 실패 (n회)"로 표시하고 15분 → 1시간 간격으로 다시 확인한다. 셸에는 사용자가 릴리스 PC에서 만든 키 쌍의 공개 키(지문 f87a108f…)가 내장되며, 비밀 키와 암호는 저장소·서버 어디에도 없다.scripts/sign-update-feed.mjs(--generate-keypair는 저장소 안에 만들지 않고 비밀 키를 출력하지 않으며, --sign, --verify는 비밀 키 없이 재검증한다). export-offline-package.ps1 -UpdateSigningKey가 서명 → 사전 검증 → electron/·updates/에 yml + .sig 쌍 복사 → 복사본 재검증 순으로 진행하며 키가 없으면 실패한다(서명 생략은 -AllowUnsignedUpdateFeed로만). release-manifest.json에 updateFeedSigned를 기록한다.js-yaml을 ^4.3.2 직접 의존성으로 올렸다(npm audit의 high 4건 해소). electron-updater 6.1.9는 올리지 않았다 — 해당 권고는 미서명 설치본·토큰 없는 피드 구성에는 적용되지 않는다.updates/에는 yml과 .sig를 한 쌍으로 둬야 하고, yml만 복사하면 모든 PC가 업데이트를 거부한다..env.production의 WR_VERSION=7.1.4 ③ 선택 AI_TIMEOUT_MS(기본 60000, 운영은 AI 휴면) ④ Electron 설치본 교체 필요(셸 변경) ⑤ 웹 Vercel은 WEB_AI_CLIENT_TOKEN·VITE_WEB_AI_TOKEN(같은 값)·UPSTASH_REDIS_REST_URL/TOKEN 등을 Production·Preview 양쪽에 등록해야 하며 vercel.json에 installCommand가 추가됐다 ⑥ 패키지·이미지 빌드 PC는 cdn.sheetjs.com 접근이 필요하다 ⑦ 패키지를 만드는 릴리스 PC에 서명 키가 필요하다(export-offline-package.ps1 -UpdateSigningKey, 키가 암호 보호면 환경변수로 암호). 서버에는 키·암호·새 환경변수가 필요 없다.active가 아닌 PC가 임시 비밀번호로 로그인하면 비밀번호 변경 뒤에도 EMR 디바이스 등록 요청이 최대 20분 늦어질 수 있다(앱 재시작이면 즉시 해소, 7.1.4 셸에는 없음). ② 7.1.3 이하 셸은 서명 검증기가 없어 서명 없는 피드로 7.1.4를 받는다 — 이 한 번의 전환은 서명 보호 밖이다(고위험 PC는 수동 설치). 서명 키를 분실하면 모든 PC에 셸을 재배포해야 하며, 설치본 자체의 코드서명(Authenticode)은 여전히 없다. ③ 내부 LLM은 GPU 미확보로 구축되지 않았고 compose의 AI_INTERNAL_* 전달은 도입 시점으로 미뤘다. ④ Electron 설치본 수동 점검(실기기 업데이트 시험 포함)은 7.1.4 최종 설치본으로 한 번에 수행하며, 통과 전에는 배포하지 않는다(docs/UPDATE_7.1.4.md "배포 전 점검").CATALOG_VERSIONRESULT_SCHEMA_VERSIONSUPPRESSION_RULE_VERSIONlift_eligiblestats_runs.limited_result 컬럼과 CHECK 2개 추가. 운영이 v7.1.1이면 v7.1.2의 0036(권한 설명 UPDATE)과 0037 두 개가 새로 적용되고, v7.1.2를 이미 배포했다면 0037 하나다) ② 운영 .env.production의 WR_VERSION=7.1.3 ③ 기존 저장 레시피·캐시는 영향 없음.unavailable_computation_failed를 돌려준다. ③ 이변량 미리보기는 방법 목록이 가려져 있을 때만 소수 셀을 푼 기준으로 바뀐다. ④ 해제된 화면에는 소수 인원이 그대로 보이므로 화면 캡처·반출·공유에 주의해야 한다..env.productionWR_VERSION=7.1.2CATALOG_VERSIONv29-exclude-from-analysistxtAppv_Sick_Cont에 신청 상병 전체가 아니라 종합소견에서 상병 상태를 "확인"으로 입력한 상병만 내보낸다(낮음 사유의 "상병 미확인"은 보지 않음). 양측 상병에서 한쪽만 확인되면 (우)/(좌)를 붙이고, 단측 상병·양쪽 모두 확인·척추·경추는 방향 없이 코드 이름만 쓴다. 방향 미선택 상병은 제외되고, 패턴 그룹 모드에서는 원본 순번(#N.)을 유지한다. 확인 상병도 CP949 한도로 절단하고 절단 필드에 기록한다. 전송 전 경고는 종합소견 한도 초과와 확인 상병(0건·한도 초과)을 확인창 한 번으로 합쳐 보여준다. EMR 헬퍼는 빈 값이면 칸을 건드리지 않으므로, 확인 상병이 0건이면 EMR에 이미 입력된 값이 남는다는 점을 경고로 알린다(헬퍼 변경 없음).high_cardinality 포함) 기술통계에서 1~9명인 소수 범주를 "기타"로 합쳐 공개한다(합이 10명 미만이면 가장 작은 공개 범주부터 끌어와 채운다). 이름 붙은 범주가 2개 미만이면 억제하고, 최빈값은 "기타"를 제외한다. 병합은 일반 기술통계 경로에서 ordinal이 아니며 요청 변수가 그 범주형 하나뿐이고 결측 인원이 소수셀이 아닐 때만 한다(다른 변수가 "기타" 안을 쪼개 보여 소수 범주가 역산되는 것을 구조적으로 막음). stats.export_limited_rows 권한자에게만 응답 시점에 원본 범주 빈도(rawLevels)를 붙이고(결과 캐시에 저장 안 함, 붙을 때 감사 기록), 동점 최빈값은 코드유닛 순서로 고정해 서버 로케일에 따라 갈리지 않게 했다. 마이그레이션 0035로 권한 표시명을 "제한 데이터 열람(원본 값·원본 분포)"로 넓혔다(키 불변).[k, k+1)으로 최댓값도 자기 구간을 갖게 했다. 정렬이 불가능하거나 검증에 실패하면 균등분할로, 그것도 유효하지 않으면 구간 수를 줄여 어떤 입력에서도 폭 0 구간 없이 건수 합이 맞는다. 가로축 눈금은 막대 경계 좌표에 두고 라벨 겹침을 방지한다. 공개용 히스토그램은 바뀌지 않았고(같은 변수라도 권한자 원본과 경계가 다를 수 있음, 화면에 안내), 결과 스키마·마이그레이션 변경은 없다.COMPLETION_ENGINE_VERSION v2 / COMPLETION_SCHEMA_VERSION 2(완료 판정 의미 변경 — 서버가 새로 검증해 기록하는 행부터 v2), CATALOG_VERSION v29-exclude-from-analysis, SUPPRESSION_RULE_VERSION v7-discrete-other-merge, RESULT_SCHEMA_VERSION v10-discrete-other-raw-levels — 기존 분석 실행 결과는 재사용되지 않고 새 규칙으로 다시 계산된다..env.production의 WR_VERSION=7.1.1 ③ 원본 값·분포를 볼 사용자에게는 7.1.0과 같이 관리자 콘솔 "통계 권한" 탭에서 "제한 데이터 열람(원본 값·원본 분포)"을 부여(이미 부여된 사용자는 그대로 유지). Electron 재설치는 필요 없다(서버 SPA 로드, EMR 헬퍼 변경 없음) — 다만 EMR 주입으로 달라지는 확인 상병·직업력은 서버 배포 후 다음 로드부터 적용된다.statsBivariateHttp welch_t 2건·statsRegressionHttp 표준화 1건 통합 테스트 실패는 v7.0.0(main)에서도 동일하게 재현되는 기존 문제다 — 별도 수정 예정. ④ 서버 DB 통합 테스트와 도커 postgres 종단 확인은 이번 변경에 대해 실행하지 않았다..env.productionWR_VERSION=7.0.0STATS_WORKBENCH_ENABLED=trueSTATS_*WR_GIT_COMMITpackage.jsonUPDATE_7.0.0.mdOFFLINE_DEPLOYMENT_PACKAGE.mdwr-app-server:7.0.0-pre 이미지 빌드 성공(app 약 2.75GB·stats venv 약 391MB), --network none selfcheck 통과, v6.5.4 스키마(0027)와 기존 환자 데이터 위에 마이그레이션 0028~0033이 자동 적용되고 기존 행은 draft로 보존됨을 확인. 시험 환자 약 1,800명으로 컨테이너 대상 HTTP 종단 검증 — 기술통계·Table 1·이변량(Welch t·Pearson)·상관행렬·회귀(OLS·이분 로지스틱)·예측(L2 로지스틱, 202→폴링→완료)·비동기 취소·CSV(기술통계·Table 1·회귀 성공, 이변량·상관행렬은 *_EXPORT_NOT_SUPPORTED 400) 통과. 권한은 기본 3종이 grant 없이 통과하고, stats.export_limited_rows는 grant 전·부여 후·회수 후에 제한 필드(pointDiagnostics·scatter.points·outlierValues) 노출이 나타났다 사라짐을 확인. 분당 20회 한도(429)와 차분 방지 예산 억제가 실제로 동작하는 것도 관측. 롤백 호환: 구버전 6.5.4 앱이 0033 스키마 DB에서 마이그레이션 없이 기동하고 로그인·환자 목록이 정상. 미검증: 실제 병원 데이터, Electron 실기 메뉴, 구형 Chrome(80) 실브라우저 렌더링.