System Admin — 법정공제 모니터링·Audit 기능 검토
system admin(tools/system-admin)에 법정공제 엔진의 정상 동작을 모니터링하고 audit할 수 있는 기능을 추가하기 위한 배치 검토. 설계 문서 design-statutory-deduction-phase1.md의 §3(누락 방지 5층)·§5(릴리스 운영)·§7 결정 3(파라미터 관리 주체 = hereby)과 짝을 이룬다.
상태: v1.1 검토안 (2026-08-20) — system admin 재설계 방향(별도 세션 진행 예정)을 반영해 갱신: ① 데이터 조회를 raw SQL 직접 쿼리에서 API 서버 경유로 전환, ② 인증을 환경변수(IP allowlist + 단일 JWT)에서 시스템 계정 인증으로 전환, ③ 하나의 system admin이 여러 API 서버 엔드포인트(enterprise client)를 전환·관리 — 엔터프라이즈별 독립 배포(API+DB)를 한 콘솔에서 관리하는 구조. 이 문서의 기능(무엇을 보여줄 것인가)은 재설계와 무관하게 유효하며, 구현 위치(§4)가 API 서버 쪽으로 이동하고 모니터의 축이 "조직"에서 "엔드포인트 × 조직"으로 확장된다(§3-0).
1. 왜 system admin인가
- 결정 3에 따라 요율·세액표의 관리 주체는 hereby(벤더)다. 조직 관리자 화면(admin 앱)이 아니라 벤더 운영 콘솔이 요율 결손·개정 지연·적재 오류를 감시할 자리다.
- system admin은 전 조직 횡단 조회가 본질인 유일한 표면이다 (멀티테넌시 격리의 의도된 예외 — IP allowlist + 전용 JWT로 보호). "어느 조직에 요율이 빠졌나"는 조직 안에서는 보이지 않는 질문이다.
- 기존에 같은 성격의 선례가 이미 있다: attendance 뷰어 탭(조직 선택 → read-only 조회)과 integrations 헬스 탭. 새 기능은 이 두 패턴의 조합이다.
2. 현재 구조 (구현 지형)
| 축 | 현재 |
|---|---|
| 서버 | 독립 Fastify (server/index.ts, 포트 4100) — 도메인별 라우트 파일(server/routes/*.ts), raw pg 쿼리(db.ts의 query()) — TypeORM/코어 로직 미사용 |
| 인증 | IP allowlist 훅 + JWT (server/auth.ts) — 단일 시스템 관리자 계정 모델 |
| UI | Alpine 탭 대시보드 — src/views/tabs/*.html include + app.ts의 tabs/activeTab/탭별 로더 |
| 선례 | /:orgId/attendance (조직 선택 read-only 뷰어), integrations (전 조직 헬스), audit_log 테이블(migration 1864) 존재 |
재설계가 이 검토의 최대 제약을 해소한다. 종전 구조(raw pg)에서는 세액 모듈·수식 엔진·computeSheetCommitStatus를 재사용할 수 없어 "계산 로직 복제 금지 — 존재·버전·집계·이력만" 경계와 규칙 사본(stale 판정 SQL) 동기화 부담이 있었다. API 서버 경유로 바뀌면 이 문제가 소멸한다: stale 판정은 computeSheetCommitStatus를, 시행일 해석은 resolveOrgConstantValues를, 표본 대조는 tax 모듈을 그대로 호출하면 되고, 화면 값과 감시 값이 같은 코드에서 나오므로 불일치 자체가 불가능해진다. §0-2의 "두 구현 공식 불일치" 재발 경로가 구조적으로 닫힌다.
3. 제안 — 「법정공제」 탭, 3단계
3-0. 멀티 엔드포인트가 모니터에 갖는 의미
엔터프라이즈 클라이언트마다 API+DB가 독립 배포라면, 요율·세액표는 엔드포인트(DB)마다 따로 적재된다. 즉 벤더 책임 모델의 실질 리스크가 "어느 조직에 요율이 빠졌나"에서 **"어느 엔드포인트에 새 요율/세액표가 아직 안 갔나"**로 한 차원 올라간다 — 이 커버리지 추적이 바로 이 모니터가 존재하는 이유가 된다.
- 조회(Phase A·B): 프로시저는 각 API 서버 안에서 "그 서버의 전 조직"을 집계한다(서버는 자기 DB만 안다). system admin이 등록된 엔드포인트들을 순회 호출해 엔드포인트 × 조직 매트릭스로 합쳐 보여준다. 기본 화면은 엔드포인트별 요약 행(최악 상태 롤업) + 펼치면 조직 상세.
- 릴리스(Phase C): "전 조직 일괄"이 "전 엔드포인트 × 전 조직 fan-out"이 된다. 부분 실패(일부 서버 다운·구버전)가 정상 상태이므로, 릴리스 결과는 엔드포인트별 성공/실패/미지원으로 집계하고 재시도 가능해야 한다.
- 버전 편차: 구버전 API 서버에는
statutoryHealth프로시저가 없을 수 있다 — 모니터는 이를 오류가 아니라 "미지원(서버 버전 X)"으로 표시해야 한다.
Phase A. 상태 모니터 (read-only, 최우선)
한 화면의 전 조직 × 헬스 매트릭스. 조용한 결손을 벤더가 먼저 발견하는 것이 목적이다.
| 지표 | 판정 | 데이터 소스 (전부 read-only SQL) |
|---|---|---|
| 요율 파라미터 | statutory.* 필수 키 10종 존재 여부 · 키별 최신 effectiveFrom · 다음 귀속월에 유효 버전 없으면 🔴 (연말 "내년 요율 미입력" 조기 경보) | organization_parameters + organization_parameter_history |
| 값 일관성 | 법정값인데 조직 간 값이 다르면 🟡 (의도적 override 인지 확인 대상 — 릴리스 스크립트 누락 감지) | 동일 |
| 간이세액표 | org별 최신 버전 · 행수(기대 7,128) · 구간 무결성(LAG 윈도우로 gap/역전 카운트) · 미적재 🔴 | simplified_tax_tables |
| 커밋 현황 | org×당월: statutory_insurance 시트 존재 · lastCommittedAt · stale(수당 재커밋 후 미재반영 — API computeSheetCommitStatus 그대로 재사용) | sheet_views + allowance_records |
| 판정 입력 결손 | 재직자 중 생년월일/주 소정근로시간 미등록 수 (판정 매트릭스가 "판정 불가"로 fail-safe 동작 중인 규모) | employees + employment_contracts |
구현: API 서버에 시스템 스코프 프로시저 1개(statutoryHealth — 전 조직 매트릭스 JSON)를 두고 system admin이 호출한다. UI는 조직 행 × 지표 열 표에 🔴/🟡/🟢. stale 판정은 computeSheetCommitStatus 재사용 — SQL 사본 없음.
Phase B. Audit 열람 (read-only)
세무사 audit 패키지(§3 Layer 5)의 화면 버전. 조직 선택 → 상세.
- 파라미터 변경 이력 타임라인 —
organization_parameter_history(namespacestatutory.%): 시행일·값·reason(근거 고시)·changedBy. "이 값의 출처"를 즉답하는 화면. - 공제 레코드 요약 + lineage 뷰어 — org·귀속월 선택: 항목별(targetColumnKey) 합계·건수·
sourceType분포, 레코드 클릭 시appliedParameters/inputSnapshot(사용된 요율 버전·산식·표값·자녀 차감) 표시. CSV export 버튼 = audit 패키지 내보내기의 1차 구현. - 검증 이력 — 표본 대조(
release-validation절차) 수행 기록은 파라미터reason과docs/internal/release-validations/커밋에 남으므로, 화면은 reason 표시로 갈음 (별도 테이블 신설은 Phase C에서 릴리스 UI와 함께).
Phase C. 릴리스 운영 도구 (write — 신중, 별도 결정 후)
§5 릴리스 워크플로의 UI화. 쓰기 도구는 다음 가드 없이는 만들지 않는다: 실행 전 미리보기(영향 조직 수·현재값 분포 diff) → 확인 다이얼로그 → audit_log 기록(기존 audit_log 테이블 재사용 — 누가·언제·무엇을) → 결과 리포트.
- 요율 일괄 릴리스:
update-statutory-parameter.ts의 UI 버전 (namespace/key/값/시행일/근거 고시 입력 → 전 조직). - 세액표 적재: CSV 업로드 → 파싱 검증 리포트(행수·무결성·표본 미리보기) → 적재. 기존 "부분 갱신 거부" 규칙 유지.
- 표본 대조 실행: 기대값 기입된 CSV 업로드 → PASS/FAIL 표. API 경유 구조에서는 tax 모듈을 직접 재사용하므로 별도 프록시가 필요 없다 —
verify-tax-samples.ts의 대조 로직을 API 프로시저로 승격하고 스크립트는 그 호출자로 남긴다. - 결손 조직 요율 시드 버튼 (Phase A 🔴 행에서 바로).
4. 구현 스케치 — 재설계 반영판
데이터·로직은 API 서버(packages/api) 에, system admin에는 화면과 호출만 남긴다.
| 위치 | 내용 |
|---|---|
packages/api — 시스템 스코프 라우터 (신규, 예: system-statutory.router.ts) | statutoryHealth(A — 전 조직 매트릭스, computeSheetCommitStatus 재사용) · statutoryParameterHistory(B-1) · statutoryDeductionSummary/deductionLineage/exportAuditCsv(B-2) · Phase C: releaseStatutoryParameter/importTaxTable/verifyTaxSamples(기존 스크립트 로직 승격) |
| system admin UI | src/views/tabs/statutory.html + app.ts 탭 등록 — 재설계된 API 클라이언트로 위 프로시저 호출 |
재설계 세션에 넘길 요구사항 (이 기능이 새 아키텍처에 요구하는 것):
엔드포인트 레지스트리와 순회 호출 기반 — 엔드포인트 목록(이름·주소·환경·서버 버전)과 엔드포인트별 시스템 계정 인증(토큰이 엔드포인트마다 발급되는지, 단일 IdP인지)이 재설계의 소관이다. 모니터는 그 위에서 "등록된 전 엔드포인트를 순회해 같은 프로시저를 호출하고 결과를 합치는" 공통 유틸(부분 실패 허용·타임아웃·버전 미지원 구분)을 필요로 한다 — 이 유틸은 법정공제만이 아니라 향후 모든 헬스류 화면이 재사용할 것이므로 재설계 쪽 공통 인프라로 두는 것을 권고한다.
크로스 org 조회 스코프 — 기존
organizationProcedure는 단일 org에 묶인다. 시스템 계정 전용 프로시저 타입(가칭systemProcedure)이 필요하며, 전 조직 집계(health)와 특정 org 상세(audit) 두 형태를 모두 지원해야 한다. 멀티테넌시 불변식("조직 간 데이터 노출 금지")의 명시적 예외이므로, 시스템 계정 여부를 토큰 위조가 아닌 계정 속성으로 판정해야 한다 (기존 Account 레이어·organization-grant/메타 계정 grant가 후보 토대).감사 주체 식별 — Phase C 쓰기 도구의 audit_log에는 "어느 시스템 계정이" 실행했는지가 남아야 한다. 환경변수 인증에서는 불가능했던 부분이며, 시스템 계정 인증 전환의 실질 수혜처다 (§7 결정 3의 "승인 여부·승인자 기록"과 접합).
read/write 스코프 분리 — Phase A·B(조회)와 Phase C(릴리스 실행)는 같은 시스템 계정이라도 권한을 나눌 수 있어야 한다 (최소: 프로시저 단위 구분, 이상적으로는 계정별 role).
착수 순서 권고: 재설계(API 경유 + 계정 인증)가 선행되어야 하므로, 이 기능은 ① API 쪽 프로시저를 재설계 세션의 시스템 스코프 위에 구현 → ② system admin 탭 UI 순으로 진행한다. 재설계 전에 급히 필요하면 Phase A 조회 프로시저만 먼저 API에 만들어 두고(스코프는 재설계 후 교체) UI를 붙이는 절충이 가능하다.
5. 결정이 필요한 것
- Phase 범위 — A만 먼저 vs A+B 함께 (권고: A+B 함께 — B의 이력·lineage 조회는 전부 read-only라 위험이 없고, audit 요구의 실체가 B다).
- Phase C의 쓰기 권한 모델 — 단일 시스템 관리자 JWT로 충분한가, 릴리스 행위에는 별도 확인(2인 확인 또는 승인 기록)을 요구할 것인가 (§7 결정 3의 "선택적 승인 기록"과 정합 필요).
stale·헬스 판정 SQL의 규칙 사본 관리→ API 경유 전환으로 해소 (같은 함수 재사용).- 알림 연계 — 🔴 발생 시 수동 확인(대시보드)으로 시작할지, 슬랙/메일 등 push 채널을 붙일지 (후속).
- 재설계와의 순서 — 시스템 스코프 프로시저의 인증 모델이 확정되기 전에 Phase A를 임시 스코프로 선행할지, 재설계 완료를 기다릴지.
- 멀티 엔드포인트 릴리스의 커버리지 기록 — 요율 개정을 엔드포인트별로 릴리스한 이력(어느 서버에 언제 갔나)을 어디에 남길 것인가: system admin 쪽 중앙 기록 vs 각 엔드포인트의 파라미터 이력으로 분산(현행). 중앙 기록이 없으면 "안 간 서버"를 매트릭스 조회로만 발견하게 된다 (사후 감지 — Phase A가 안전망이지만, 릴리스 시점 기록이 더 이르다).