Skip to content

법정공제 시스템 설계 (세무 처리 1차) — 기획 초안 ​

급여정산의 1차 세무 처리로서, 국내 규제에 해당하는 급여 법정공제 항목(4대보험·소득세·지방소득세 등)을 시스템이 계산하도록 만들기 위한 기획 문서. 향후 세무 시스템(원천세 신고·연말정산 등)으로의 확장을 전제로 하되, 이 문서의 범위는 월 급여 공제 계산까지다.

상태: v2 (2026-08-20) — §7의 결정 8건 확정. PoC 6차까지 구현·머지(PR #579)되어 4대보험·소득세가 실제로 계산·커밋된다. 개발 착수 전 선행 과제: 법적 책임 구조 법률 검토 (§7 결정 4).

읽는 순서 주의. §0(현황)과 §4-5(선결 수리)는 2026-08-19 착수 시점의 스냅샷이다. 그 뒤 PoC가 4대보험·소득세를 동작시켰으므로, 지금 무엇이 되고 무엇이 안 되는지는 §6-1~§6-7과 각 절의 현황 표기를 봐야 한다. §0만 읽으면 "계산 경로가 하나도 없다"로 오해한다.

핵심 요구사항:

  1. 공제 계산은 기존 sheet 기능 위에서 동작한다.
  2. 관련 법령·시행규칙을 면밀히 반영하고, 개정 시 지속적으로 갱신할 수 있어야 한다.
  3. 공제 항목이 시스템에 제대로 반영되고 있는지 세무사가 audit할 수 있어야 한다.
  4. 무엇보다 공제 항목이 빠지지 않고(누락 없이) 적용되는 방식이어야 한다.

문서 구성: §0 현황 → §1 무엇을 계산해야 하나(도메인) → §2 남들은 어떻게 하나(리서치) → §3 누락을 어떻게 막나(핵심) → §4 hereby에 어떻게 얹나(설계) → §5 운영 → §6 로드맵 → §7 결정 사항.


0. 현황 — 코드와 문서에 지금 있는 것 ​

2026-08-19 코드베이스 전수 조사 결과 요약. 파일·라인 단위 상세는 조사 보고서 원문 참조.

한 줄 요약 (착수 시점): 공제 레코드를 담는 원장(ledger)과 시트 커밋 파이프라인은 완성돼 있지만, 법정공제 금액을 실제로 계산해서 그 원장에 넣는 경로는 현재 하나도 살아있지 않다.

→ 2026-08-20 현재: 이 공백은 PoC로 메워졌다. 4대보험·소득세가 시스템 템플릿의 수식/모듈로 계산되고 원장까지 커밋된다(§6-1~§6-7). 아래 두 표는 당시 진단 기록으로 남기되, 각 행의 현황 열이 지금 상태를 가리킨다.

0-1. 이미 있는 것 (이 기획이 재사용할 자산) ​

자산상태
공제 원장 DeductionRecordsheet lineage 3필드(sourceSheetId/RowId/ColumnKey) NOT NULL + 유니크 인덱스. 시트 "✓ 커밋"이 유일한 생성 경로 (직접 create API는 의도적으로 throw). 감사 필드 inputSnapshot·appliedParameters·formulaVersion 보유
커밋 파이프라인 commitSheetsweep-then-insert(재커밋 안전), 수식 컬럼도 리졸브 후 커밋, 다중 직원 분배, 급여대장 aggregate 앵커(targetSheetSlug/targetColumnKey) 합산까지 완결
파라미터 스토어 OrganizationParameter + OrganizationParameterVersion엔티티·라우터·SDK·admin 설정 UI 전부 존재, 실사용 중(인센티브). Version에 effectiveFrom이 있어 시행일별 이력 저장 가능
수식 엔진의 시점 정합 파라미터 참조시트 수식 [$namespace.key]가 시트 귀속월 기준으로 그 시점에 유효했던 파라미터 버전을 자동 선택 (resolver.ts L2938~). 요율 개정 대응의 핵심 메커니즘이 이미 구현돼 있다
수식 엔진 일반24개 함수(MIN/MAX/ROUND/IF 포함) + SUMIF(S)/COUNTIF(S) + 크로스시트 참조 + 사이클 감지 — 상하한(cap/floor) 같은 산식을 수식만으로 표현 가능한 수준
codeModuleinsurance.ts(4대보험 요율 4개 파라미터화) / tax-bracket.ts(누진 직산) + 컬럼 설정 UI(모듈·키 선택, 파라미터 폼)까지 존재
연도별 세율 구간 테이블TaxBracketConfig (effectiveYear + CRUD) — 유일하게 실재하는 법정 요율 테이블

0-2. 없는 것 / 깨진 것 (진단 = 2026-08-19) ​

현황 열은 2026-08-20 재검토 결과다. 10건 중 5건이 PoC로 해소됐고, 4건이 남았다.

문제내용현황 (2026-08-20)
🔴 4대보험·소득세 computed 컬럼이 서버에서 동작 안 함compute.ts의 입력 로더가 "insurance"/"tax_bracket" 문자열로 분기하는데 enum 실제 값은 "payroll_rate_config"/"tax_bracket_config" — 모든 분기를 통과 못 해 빈 입력을 받고 조용히 아무 셀도 안 씀. UI에서는 모듈 선택이 가능하게 보임✅ 해소 (§6-1)
🔴 DeductionPipeline 미실행generateForEmployee() 호출부가 레포 전체에 0건 — 메타데이터 조회에만 쓰임⬛ 설계상 폐기 — 결정 6(수식 일원화)이 이 경로를 대체한다. 호출부는 여전히 0건이며 정리 대상
🔴 국민연금 상·하한 미구현codeModule 주석이 "상하한은 elsewhere에서 적용"이라 하나 그 코드는 존재하지 않음✅ 해소 — 템플릿 수식의 MIN/MAX 클램프 + 「연금 기준소득월액」 열로 가시화 (§6-1)
🔴 간이세액표 전무레포 전체 grep 0건. 현행 tax-bracket은 "월급×12 연환산 후 누진세율 직산" — 실제 원천징수 방식(간이세액표 조회 + 부양가족 수)과 다름✅ 해소 — 공식 실표 7,128행 적재 + 자녀 차감 + 1,000만 초과 산식 (§6-6·§6-7)
🔴 유일하게 "동작하는" 4대보험 = seed 템플릿 하드코딩 수식2024년 요율이 수식에 박제(* 0.045 등). 커밋 컬럼 미설정이라 원장에 도달하지도 않음. 장기요양 공식이 codeModule(건보료×12.95%)과 seed 수식(급여×0.4591%)에서 서로 다름🟡 부분 — 신규 법정공제 템플릿은 [$statutory.*] 참조. 그러나 퇴사정산 템플릿에는 2024년 요율이 그대로 박제돼 있다 (seed-sheet-templates.ts L2147 설명·L2190·L2199)
🔴 commitSheet가 sourceType/formulaVersion 미기록공제 출처 구분 불가 + 소득세 계산의 priorDeductions 필터가 NULL 비교로 무력화✅ 해소 (§6-1)
🔴 연말정산·퇴사정산 → 공제 반영 경로 2곳이 NOT NULL 위반으로 실패year-end-settlement.router.ts L290~, resignation-record.router.ts L390~ — lineage 없이 INSERT 시도❌ 미해소
❌ 법정 vs 임의 공제 구분 수단 없음DeductionSubCategory enum은 정의만 되고 사용처 0건. 구분은 reason 문자열뿐❌ 미해소 (§4-4 과제)
❌ 요율 데이터 없음4대보험 요율을 담은 organization_parameters seed 없음, namespace 명명 규약 미정. core의 INSURANCE_RATES 상수는 프로덕션 사용처 0건인 dead constant✅ 해소 — statutory.* 7종 시드 + 전 조직 일괄 갱신 스크립트 + 신규 조직 자동 시드 (§6-1·§6-5 ④)
🟡 eCount 공제 import = 미완성 스캐폴드현재 기획돼 있던 운영 경로: hereby→eCount로 수당 export → eCount 급여모듈이 공제 계산 → xlsx를 다시 import→커밋. 스테이징·커밋 배관은 있으나 컬럼 매핑 파서 미완성 (§7 방향 D)❌ 미해소 (결정 2 — 모드의 하나로 유지)

0-3. 문서에 정리된 상태 ​

문서상태
payroll-schema-refactor.md골격은 이 기획과 정합: 금액은 원장에만, "요율·배수·단가는 Layer 2(sheet column metadata + org parameter store)에만" 원칙 명시. 단 요율을 어떤 namespace로 저장할지, 연도별 개정을 어떻게 처리할지는 미정의. 미결 #5: 휴직 시 보험료 일할계산 calculator 부재
payroll-provenance.md일부 stale — PayrollRecord에 국민연금 등 개별 컬럼이 있다고 서술하나 실제 엔티티엔 totalDeductions뿐, itemCodeId drop(migration 1816) 미반영
calculators.mdDeductionPipeline 흐름을 서술하지만 그것이 실행되지 않는다는 사실은 없음
korean-payroll.md (guides)4대보험 요율·세율표 참고 자료 — 코드와 연결 없는 정책 문서
간이세액표·요율 개정 프로세스 문서전무 → ✅ 신설됨: statutory-deduction-release-validation.md(홈택스·공단 표본 대조 절차 + 러너), legal-review-questions-statutory-deduction.md(결정 4 질의서), packages/database/data/README.md(실표 출처·변환 검증 체크리스트)

정리: 문서의 아키텍처 원칙(3계층, 요율은 L2)은 유효하고 이 기획의 전제로 삼을 수 있으나, "법정공제를 실제로 계산하는 층"은 문서·코드 모두에서 설계 공백이다. 이 문서가 그 공백을 채운다.


1. 계산해야 하는 것 — 법정공제 도메인 지도 ​

이 절의 모든 요율·금액은 2026-08 리서치 기준이며 구현 시 관보·공단 고시로 재확인해야 한다. 바로 이 "수치가 계속 바뀐다"는 사실이 §3~§5 설계의 출발점이다.

1-1. 공제 항목 전체 목록 ​

상시 법정공제 — 모든 상용 근로자 급여에서 매월 공제 (6종)

#항목2026 요율/방식 (근로자 부담분)근거 법령관장 기관
1국민연금기준소득월액 × 4.75% (하한 41만·상한 659만)국민연금법 §88국민연금공단
2건강보험보수월액 × 3.595%국민건강보험법 §69·70건보공단
3장기요양보험건강보험료 × 13.14% ÷ 2 (고지서는 건보료 연동 방식)노인장기요양보험법 §9건보공단
4고용보험보수 × 0.9% (실업급여 계정만 — 고안·직능 요율은 사업주 전용, 근로자 전가 시 위법)보험료징수법 §13·14근로복지공단
5소득세간이세액표 조회 (월급여 구간 × 공제대상 가족 수 × 자녀 수) + 근로자별 80/100/120% 선택소득세법 §134, 시행령 §194·별표2국세청
6지방소득세소득세 × 10% (절사 규칙 별도)지방세법 §103조의13지자체

상황부 법정공제 — 조건 충족 시 공제 의무 발생

항목트리거계산 방식
학자금 상환(ICL) 원천공제국세청이 회사에 통지 (6월)통지된 연 상환액 ÷ 12를 7월~다음해 6월 공제
급여 압류·가압류법원 명령 수령구간식 계산, 압류금지 하한 월 250만 (2026.2.1~)
건강보험 연말·퇴직 정산분매년 4월 / 퇴직 시공단 정산 고지액
소득세 연말정산 추납/환급매년 2~3월 급여연말정산 결과액

법정공제가 아닌 것 (혼동 주의)

  • 산재보험은 전액 사업주 부담 — 급여명세서 공제란에 나타나면 그 자체가 오류. 시스템이 이를 구조적으로 막아야 한다.
  • 임의공제 (노조비·사우회비·사내대출 상환, 그리고 라온 미팅 §5의 상품권·기숙사·주차장 공제 등): 근로기준법 §43 전액불 원칙상 법령 또는 단체협약 근거 없는 공제는 위법. 법정공제와 별도 카테고리로 분리하고, 임의공제 항목에는 근거(동의) 메타데이터를 요구해야 한다.

1-2. 계산 유형은 3가지뿐이다 ​

모든 법정공제는 다음 3가지로 환원된다. 이 분류가 엔진 설계의 골격이 된다.

유형항목시스템이 필요로 하는 것
(a) 요율 계산형국민연금·건강·장기요양·고용시행일별 요율 + 상하한 + 기초금액(비과세 제외 보수)
(b) 조회표형소득세(간이세액표) + 지방소득세(×10%)시행일별 구간표(수천 행) + 부양가족 수 축 + 절사 규칙
(c) 통지 금액형ICL·압류·정산분·공단 고지액 오버라이드외부에서 통지된 금액/기간의 입력·분할·종료 관리

1-3. "잠정 징수 → 정산" 이중 구조 ​

월 공제액은 대부분 확정액이 아니라 잠정액이다:

  • 건강보험: 신고 보수월액 기준 잠정 부과 → 3월 보수총액 신고 → 4월 정산(추가징수/환급). 퇴직 시 퇴직정산.
  • 소득세: 간이세액표는 잠정 징수 — 연말정산이 확정.
  • 국민연금: 5월 소득총액 신고 → 매년 7월 기준소득월액 갱신으로 공제액 변동.

"월 공제"와 "정산 이벤트 공제"는 별개 모델이다. Phase 1 범위는 월 공제이되, 정산분이 (c) 통지 금액형으로 들어올 자리는 처음부터 둔다.

1-4. 규칙이 바뀌는 주기 — 서로 다르다 ​

항목변경 주기최근 사례
국민연금 요율법 개정 시 — 2026~2033년 매년 0.5%p 인상 스케줄 확정 (9.5%→13%)2026.1월 9.5%
국민연금 상·하한매년 7월 1일2026.7월 41만/659만
건강보험 요율매년 (전년 말 고시 → 1월 1일 적용)2026년 7.19%
장기요양 요율매년 (전년 말)2026년 13.14%
고용보험 요율비정기2022.7월 이후 유지
간이세액표비정기 — 연중 개정 가능2026년은 3월 1일 지급분부터 신표 (1~2월은 구표)
압류금지 하한비정기2026.2.1 185만→250만

같은 해 안에서도 항목마다 시행일이 다르다. 연 1회 일괄 업데이트로는 대응 불가 — 파라미터마다 독립된 시행일 버전이 필수인 이유다. 다행히 §0-1대로 시점 정합 조회 메커니즘은 이미 있다.

1-5. 특수 케이스 체크리스트 ​

적용/제외 판정 로직(§3 Layer 1)이 알아야 하는 조건들. 상세 수치·근거는 리서치 원문 참조.

  • 일용직·단시간: 보험별 가입 기준이 다름 (국민연금 월 8일/60시간/소득 기준, 건보 월 8일, 고용보험은 주 15시간 미만 초단시간 제외). 일용근로 소득세는 간이세액표가 아닌 분리과세 별도 산식.
  • 60세 이상: 국민연금 자격 상실 → 공제 중단 (계속 근무해도). 건강·고용은 연령 무관 (65세 이후 신규 고용 시 고용보험 실업급여 미적용).
  • 이중근로: 국민연금 상한 초과 시 사업장별 안분 — 자사 데이터만으로 계산 불가 → 고지액 오버라이드 경로 필수. 고용보험은 주 사업장에서만.
  • 비과세 소득: 식대 20만·자가운전보조금 20만 등은 소득세뿐 아니라 4대보험 산정 기초에서도 제외 → 급여 항목에 "비과세 여부+한도" 속성 필요. 일부 항목은 보험별 포함 여부가 달라 예외 테이블 재확인 필요.
  • 휴직: 부분월 휴직 시 기본급 일할계산 → 보험료 기초 변동 (payroll-schema-refactor 미결 #5와 동일 항목).
  • 두루누리: 지원금은 고지서에서 차감 — 공제 계산은 정상 산출, 명세서 표시 방식은 정책 결정 필요.
  • 피부양자: 건보 공제액에 영향 없음 (부양가족 수는 간이세액표에만 영향).

2. 타 시스템 리서치 — 규칙을 최신·누락 없이·감사 가능하게 유지하는 법 ​

2-1. Rules-as-code: 파라미터와 수식의 분리 (OpenFisca) ​

프랑스 정부 오픈소스 OpenFisca는 법령 규칙 코드화의 사실상 표준 참조 모델이다 (프랑스·뉴질랜드·캐나다 정부 채택).

  • 요율·상한·기준액은 코드가 아니라 파라미터 — 법이 바뀌면 코드는 그대로 두고 파라미터에 새 시행일 값만 추가.
  • 파라미터는 시행일별 값의 목록: 2025-01-01: 7.09%, 2026-01-01: 7.19% … 계산은 항상 "귀속 기간"을 받아 그 시점의 유효값을 조회 → 과거 월 재계산 시 자동으로 당시 요율 적용.
  • 파라미터마다 법령 근거 메타데이터(metadata.reference) — 감사 시 "이 값의 출처"를 즉답.
  • 누진 구간표는 1급 타입 — threshold+rate 목록, 구간 값도 개별 버전 관리.
  • 폐지/미입력 파라미터는 명시적 에러 — 유효값 없는 기간의 계산은 0이 아니라 실패. 조용한 누락을 타입 수준에서 차단.
  • 회귀 검증은 입력→기대 산출의 선언적 YAML 테스트를 규칙 저장소에 동거시키는 방식.

참고: 법조문 대응 검증 언어 Catala는 미국·프랑스 세법을 코드화하는 과정에서 공식 구현체의 버그를 발견 — 법조문을 구조적으로 코드화하는 행위 자체가 검증 수단이 된다.

2-2. 상용 엔진: 세율표는 벤더 책임 + 버전 릴리스 ​

  • QuickBooks: 세율표를 버전 번호 + 릴리스 노트가 붙은 payroll update 패키지로 배포 ("릴리스 22613: 신규 원천징수 테이블, 주 실업보험 상한 조정…").
  • ADP: 전담 조직이 11,000여 관할구역 법령을 상시 모니터링. Gusto: 계산·신고·납부를 통째로 서비스로 흡수.
  • Symmetry Tax Engine (급여 SaaS들이 임베드하는 계산 전용 엔진): 핵심은 계산기가 아니라 연구 조직 — 전임 세무 리서치 팀이 고시를 모니터링해 요율 테이블로 컴파일하고, 변경 시 구독 고객에게 알림 발송. 상위 티어는 "왜 이 세액인가" 설명 기능까지 상품화 (문의 티켓 74% 감소 주장).

공통 패턴: 요율 데이터 + 변경 알림 + 설명가능성이 하나의 상품 단위.

2-3. 한국 서비스 ​

서비스접근 방식
더존 (Smart A/WEHAGO)요율·간이세액표·공휴일을 프로그램 업데이트로 배포, 4대보험 자동계산(두루누리 감면 포함), WEHAGO T로 4대보험 일괄신고 연계
뉴플로이"법·요율이 바뀌어도 사용자는 아무것도 안 해도 된다"를 명시적으로 홍보. 파트너 회계법인이 원천세 신고·납부까지 수행 — 전문가 검수·신고 책임을 플랫폼 뒤에 결합
flex법 개정 자동 반영 명시 + 공제 산정 방식의 다중화: 4대보험을 (a) 공단 고지서 업로드 (b) 보수월액 기준 산정 (c) 수동 업로드 중 조직이 선택. 절사 정책·계산 방식도 조직별 설정으로 노출
시프티매년 "요율 변경 총정리" 콘텐츠 발행 — 요율 최신성을 마케팅 자산화

한국 시장에서 "요율 자동 업데이트"는 벤더 책임으로 흡수하는 것이 표준 기대치. 차별화는 (1) 신고·납부까지 수직 통합, (2) 세무사/회계법인 파트너 검수, (3) 컴플라이언스 콘텐츠.

특히 flex의 "정률 계산 vs 고지 기반" 이원화는 중요한 통찰이다: 4대보험의 진실 원천은 결국 공단 고지액이고, 정률 계산은 그 예측치다. 시스템이 이 차이를 인지해야 한다 (§1-3, §3 Layer 4).

2-4. 정합성 검증 기법 ​

  • HMRC PAYE Recognition (영국): 정부가 표준 테스트 시나리오를 제공해 급여 소프트웨어를 검사·인증 목록 게시 — "정부가 golden test set을 주는" 모델. 한국엔 이 제도가 없지만, 국세청 간이세액표 공표값·홈택스 모의계산·공단 모의계산기가 사실상의 golden 소스 역할을 할 수 있다.
  • 간이세액표는 기계가독 소스가 존재: 공공데이터포털 파일데이터(Excel/CSV) + 홈택스 배포. 실시간 API는 없음 → "파일 파싱 → 스테이징 → 사람 승인 → 반영" 반자동 파이프라인이 현실적.
  • 병행운영(parallel run): 급여 시스템 전환의 표준 검증 절차 — 기존 계산(수기/세무사/타 시스템)과 신규 모듈을 동일 기간 동시 운영, gross·공제·net을 라인 단위 대조, 불일치를 원인별 분류·해소 후 절체.
  • SAP 소급 처리(retro accounting): 과거 확정분은 불변으로 보존하고, 소급 변경은 재계산 차액을 당기 급여에 별도 항목으로 반영. 직원별 소급 하한일로 마감 통제. "확정분 보존 vs 재계산" 논쟁의 업계 표준 답.

3. 핵심 문제: 공제 항목 누락을 어떻게 막는가 ​

누락은 세 층위에서 발생하고, 층위마다 방어 장치가 다르다. §0-2의 "모듈 키 불일치로 셀이 조용히 안 써지는" 버그가 바로 이 시스템의 실물 사례다 — 조용한 실패를 구조적으로 불가능하게 만드는 것이 이 설계의 제1원칙이다.

누락 층위예시방어
항목 누락 — 있어야 할 공제가 아예 없음조직 세팅 시 장기요양 누락; ICL 통지 미반영Layer 1
값 누락/부패 — 항목은 있는데 요율이 구버전2027년 급여를 2026년 건보 요율로 계산Layer 2
적용 오류 — 요율은 맞는데 대상 판정이 틀림60세 직원에게 국민연금 공제 지속; 비과세 미제외Layer 3~5

Layer 1 — 법정공제 레지스트리와 전수 판정 (구조적 방어) ​

시스템이 아는 법정공제 항목의 전체 카탈로그를 시드 데이터로 유지한다 (§1-1의 표가 초기값). 각 항목은 적용 판정 조건(고용 형태·연령·근로 시간 등)과 계산 유형(§1-2)을 갖는다.

핵심 규칙: 월 급여 확정 시, 재직자 전원 × 레지스트리 전 항목에 대해 "적용/제외" 판정을 강제 수행하고 결과를 기록한다. 제외는 반드시 사유 코드(60세 도달·초단시간·이중근로 부사업장…)와 함께 남는다. "판정 자체가 없는" 상태 — 조용한 누락 — 를 만들 수 없게 하는 것이 목적이다. 세무사 audit은 이 판정 매트릭스(전원 × 전 항목 × 적용/제외+사유)를 보는 데서 출발한다.

[정정 2026-08-20 → 확정 2026-08-21] 판정 없는 확정을 막는다 — 단, 판정 불가는 확인으로 ​

⚠️ 이 절은 두 번 바뀌었다. 2026-08-20 에 fail-safe(그냥 통과) → 차단으로 되돌렸고, 2026-08-21 기획 확정(§7 ①)으로 차단 → 확인이 됐다. 아래 표의 T1 이 그 최종 상태다. 「차단」이라고 적힌 옛 서술이 남아 있으면 이 경고가 우선한다.

PoC 구현(§6-4 ①)은 fail-safe를 택했다: 제외 판정이 명시적으로 참일 때만 공제를 0으로 만들고, 판정 입력이 없으면 공제를 그대로 계산해 통과시킨다. 이는 위 핵심 규칙("판정 자체가 없는 상태를 만들 수 없게")과 정면으로 어긋난다.

결론: 기획을 구현에 맞추지 않고, 구현을 기획에 맞춘다. 판정 없이 확정된 급여를 되돌리는 비용이, 판정을 요구해 확정을 늦추는 비용보다 크다.

다만 막는 것과 확인받는 것은 다르다. 2026-08-21 확정(§7 ①)은 판정 불가를 막지 않는다 — 직원 한 명의 생년월일이 비었다고 그 달 급여 전체가 멈추는 것은 실무에서 감당되지 않는다. 대신 몇 명인지 반드시 보고 넘어가게 하고, 넘어간 사실을 리포트에 남긴다. 금액이 틀리게 되는 조건(기초 부재)만 막는다.

#작업상태
T2제외 사유를 원장에 동결 — DeductionRecord.exclusionReason + 열 메타 exclusionReasons(사유별 판정 열 매핑)✅ 구현 (migration 1886)
T1판정 불가 행은 막지 않고 확인받는다 — 반영 직전 「판정 불가 N명」 확인 + 결과 리포트 기록 (열 메타 commitWarnings, 마이그 1890). ⚠️ 2026-08-20 에 차단으로 구현했다가 기획 확정 ①로 뒤집었다✅ 구현 (확인 방식)
T6기초 부재는 그대로 막는다 — 보수월액이 없으면 하한 클램프로 최소 보험료가 조용히 계산된다(§4-2b). 판정 불가와 갈리는 기준은 "넘어가면 금액이 틀리는가"✅ 구현 (차단 유지)
T3자동 판정 오버라이드 입구 — 이중근로·일용직 등 예외를 사람이 뒤집고 사유를 남긴다✅ 구현

순서는 T2 → T1 → T3: 사유를 담을 자리가 먼저 있어야 게이트가 "무엇이 판정됐고 무엇이 아닌지"를 판단할 수 있고, 오버라이드도 그 위에 얹힌다.

fail-safe 실험이 드러낸 요구사항은 차단을 복원한 뒤에도 그대로 유효하다 — 막는 것만으로는 부족하고, 왜 막혔는지가 보여야 관리자가 풀 수 있다:

  1. 판정 불가는 값이 아니라 상태로 표시한다. 현재는 생년월일 미등록이 「연령 = 0」이라는 숫자로 나타난다 — 0은 빈칸이 아니라 값처럼 보이고, 수백 행에서 사람이 눈으로 찾아내야 한다. PoC가 실제로 겪은 사고(보수 0인 직원에게 하한 클램프가 걸려 국민연금 19,475원 계산, §6-1)가 이 구조다.

  2. 판정 불가 건수는 집계돼 커밋 흐름에 나타난다. 막지는 않되, 확정 전에 "판정 불가 N건"이 보이고 그 행으로 갈 수 있어야 한다. — 🟡 부분 구현. 건수와 **대상 직원(이름·사번)**은 확인 다이얼로그에 뜨지만, 그 행으로 이동하는 링크는 없다. 사용자가 시트에서 직접 찾아야 한다.

  3. 제외 사유는 원장에 남는다. 현재 판정은 시트 수식 열의 True/False로만 존재하고 DeductionRecord에는 사유가 기록되지 않는다 — 이 상태로는 audit 패키지의 판정 매트릭스(Layer 5)를 만들 수 없다.

  4. 자동 판정을 사람이 뒤집는 입구가 있어야 한다. — ✅ 구현 (T3). 시트에 「수동 판정」 select 열(강제 적용 / 제외 — 이중근로 부사업장 / 제외 — 기타)을 두고, 판정 수식이 자동 조건보다 수동을 우선한다. 수동 제외는 사유 매핑에서도 자동 사유보다 앞서 기록되고, 수동 판정이 있으면 T1 게이트가 풀린다(탈출구). 일용직 월 8일 기준·피부양 전환의 자동 판정은 여전히 미반영이나, 이제 사람이 처리할 수 있다.

    ⚠️ 수식 엔진의 빈 셀 함정 — 참조한 셀이 비면 수식 전체가 N/A(null)가 되고, 숫자 포맷 열만 빈 참조를 0으로 읽는다. 「수동 판정」은 평소 비어 있으므로 판정 열이 그것을 직접 참조하면 거의 모든 행에서 판정이 N/A가 되어 자동 제외가 통째로 무력화된다(60세 직원도 공제가 계산된다). 그래서 숫자 포맷 열(mjCode)로 한 번 받아 0/1/2/3으로 굳힌 뒤 그것만 참조한다. 판정 게이트 열도 같은 이유로 숫자 포맷이어야 한다 — 그 열이 잡아야 하는 것이 바로 "생년월일이 비어 있음"인데, boolean 포맷이면 잡아야 할 케이스에서만 판정이 사라진다. golden test에 회귀 가드를 박아 뒀다.

표시 형식(무엇을 어떤 모양으로 보여줄지)은 §4-7에서 정의한다.

Layer 2 — 시행일 버전 파라미터 + 유효값 없으면 계산 실패 ​

OpenFisca 패턴이며, hereby에는 저장소(OrganizationParameterVersion.effectiveFrom)와 시점 조회(수식 [$ns.key])가 이미 있다. 보강할 것은 실패 정책이다: 귀속월에 유효한 파라미터가 없으면 0이나 live 값 폴백이 아니라 명시적 에러. 2027년 1월 급여 계산 시 2027년 건보 요율이 미입력이면 계산이 실패하고 관리자에게 알림 — 구요율로 조용히 계산되는 사고를 차단한다. 여기에 만료 예고(12월에 "내년 요율 미입력" 경고)를 붙이면 운영 알림(§5)으로 전환된다.

현황 (2026-08-20) — 셋 중 하나만 구현됐다.

요소상태
커밋 거부✅ 구현 (§6-5 ②) — 귀속월 유효값이 없으면 커밋을 막는다
화면 표시❌ 미구현 — 셀은 기존 N/A로만 남는다. 즉 커밋을 눌러야 문제를 알게 된다
관리자 알림 · 만료 예고❌ 미구현

또한 커밋 거부 문구가 책임 주체와 어긋난다: 결정 3은 요율 관리를 hereby 전담으로 정했는데, 현재 메시지는 조직 관리자에게 「관리자 설정 → 파라미터에서 등록해 주세요」라고 지시하고 내부 키를 원문(statutory.national_pension.employee_rate)으로 노출한다 (compute.ts L1089). 문구 수정 필요.

조용한 누락 — ✅ 해소 (T5). 소득세는 표/구간이 없거나 조회 키(부양가족수)가 비면 모듈이 출력을 내지 않아 셀이 비고, 커밋은 값 없는 셀을 조용히 건너뛴다. 경고는 서버 logger.warn뿐이라 화면에 흔적이 없었다 — "조용한 0"은 막았지만 "조용한 항목 누락"이 남아 있었다(§3 표의 첫 층위 그 자체).

실측(2026-08, 데모 병원 18명): 소득세·지방소득세가 전 직원 미계산인데 커밋이 72건 성공으로 끝났다. 4대보험만 들어간 급여가 확정된 것이다.

조치 — 커밋 열 메타에 **requireValueOnCommit**를 두고, 그 열에 빈 값이 있으면 커밋을 거부한다(파라미터 게이트와 같이 sweep 이전). 법정공제 템플릿의 커밋 열 6종에 적용했다. 0원은 걸리지 않는다 — 소액부징수·제외 판정처럼 "계산해 보니 0원"은 정당한 값이고, 막아야 하는 것은 "계산 자체가 안 된 상태"다.

검증: 세액표 미적재 상태 → 거부(「소득세 18행, 지방소득세 18행」) → 실표 적재 + 부양가족수 입력 + runCompute → 108건 커밋, 스킵 0.

Layer 3 — Golden test set (규칙 갱신마다 자동 회귀) ​

국세청 간이세액표 공표 예시값, 공단 모의계산기 값, 실제 고지액 사례를 입력→기대 공제액의 표준 케이스로 축적. 파라미터 갱신·수식 변경 때마다 전체 케이스를 자동 재실행 (HMRC/OpenFisca 모델). §1-5의 특수 케이스마다 최소 1개의 golden case를 요구하는 규칙을 두면 카탈로그와 테스트가 함께 자란다. 이 리포지토리의 "silent breakage를 테스트하라"는 기존 테스트 정책과 정확히 같은 문제의식이다.

Layer 4 — 고지액 대사 (reconciliation) ​

4대보험의 진실 원천은 공단 고지액이다. 매월 공단 고지서(EDI/파일 업로드)와 시스템 계산치를 직원 단위로 자동 대조하고 차이 리포트를 생성한다. 차이는 정상 사유(기준소득월액 차이·정산분·두루누리)로 분류되거나, 미분류 차이로 남아 조사 대상이 된다. flex의 "고지 기반" 모드가 이 문제의식의 다른 형태다.

Layer 5 — 병행운영 + 세무사 audit 패키지 ​

  • 도입 조직은 최소 2~3 급여 주기를 기존 계산(현재는 eCount/세무사)과 병행운영, 라인 단위 대사 리포트로 불일치 0 확인 후 절체.
  • audit 패키지: 세무사에게 (1) 판정 매트릭스, (2) 공제액별 계산 근거 스냅샷 — 사용된 파라미터 버전·요율·기초금액·산식, (3) 파라미터 변경 이력(누가·언제·근거 고시), (4) 고지액 대사 리포트를 내보낼 수 있게 한다. "재현 가능한 계산"이 audit의 전제 조건이며, hereby의 inputSnapshot/appliedParameters 커밋 스냅샷이 그 토대다.

4. hereby 설계 제안 — sheet 위에 어떻게 얹나 ​

기존 자산(§0-1)을 최대한 재사용한다. 신규 엔진을 만드는 것이 아니라, 이미 있는 파라미터 스토어·수식 엔진·커밋 파이프라인을 법정공제용으로 조립하고, 죽어 있는 경로를 수리하는 그림이다.

4-1. 요율 저장: 파라미터 스토어 + statutory.* namespace ​

OrganizationParameter/Version을 그대로 쓰되 namespace 규약을 확정한다 (예시):

statutory.national_pension.employee_rate   = 4.75      (%)
statutory.national_pension.income_floor    = 410000    (원)
statutory.national_pension.income_ceiling  = 6590000   (원)
statutory.health_insurance.employee_rate   = 3.595     (%)
statutory.long_term_care.surcharge_rate    = 13.14     (건보료 대비 %)
statutory.employment_insurance.employee_rate = 0.9     (%)
statutory.local_income_tax.rate            = 10        (%)
  • 시행일 관리는 effectiveFrom 버전으로. 각 버전에 근거 고시 URL/조문 메타데이터를 붙인다 (OpenFisca metadata.reference 패턴 — Version 엔티티에 reference 필드 추가 검토).
  • 글로벌 vs 조직 문제: 법정 요율은 전 조직 동일인데 현 스토어는 org 단위다. 선택지는 (i) 시스템 기본값(글로벌) 계층 신설 + org 미설정 시 폴백 ← 권고: 갱신 1회로 전 조직 반영, org 단위 override도 허용, (ii) 전 조직에 seed 복제 + 중앙 갱신 잡. (i)가 §5 운영 모델("hereby가 벤더로서 요율 관리")과 맞는다.

4-1b. 검토한 대안: "공제 파라미터 시트" — 요율을 스토어가 아닌 시트에 두는 방식 ​

요율·상하한을 별도의 파라미터 시트(행 = 항목, 열 = 값)에 두고, 공제 시트 수식이 크로스시트 참조로 끌어오는 방식. 기술적으로 지금 바로 가능하다 — 수식 엔진에 직원 무관 VLOOKUP 형태 [시트:값컬럼:키컬럼:내키컬럼]이 이미 있고(resolver.ts L3664~), 치료일 시트가 관리자 편집 단가 시트에서 단가를 끌어오는 동일 패턴이 프로덕션에서 쓰이고 있다. 시트 slug 해석이 호스트 시트의 (연,월) 기준이라, 월별 파라미터 시트를 두면 과거 월 재계산 시 그 달의 시트 값이 쓰이는 시점 고정도 자연히 된다.

기준파라미터 시트파라미터 스토어 [$statutory.*]
구현 소요최소 (기존 패턴 그대로)소 (글로벌 계층 + 실패 정책 추가)
요율 가시성◎ 시트에서 직접 보고 수정○ 수식 칩 호버(기존 org-parameter-tip) + 읽기전용 뷰 필요
중앙 갱신✗ 요율이 조직×월 시트 사본에 흩어짐 — 개정 시 전 조직 시트를 고칠 방법이 없음 (벤더 책임 모델과 충돌)◎ 글로벌 기본값 1회 갱신 → 전 조직 반영
누락 시 거동✗ 미해결 크로스시트 참조는 0으로 평가 (resolver.ts L1388 명시) — 시트 부재·키 오타·carry-forward 갱신 누락이 전부 "공제 0원"으로 조용히 통과◎ "유효값 없으면 명시적 실패" 정책을 얹을 수 있음 (Layer 2)
시행일 정밀도△ 월 복사 단위 — 7월 상하한 갱신 등을 사람이 그 달 시트에서 바꿔야 하고, 안 바꾸면 구값이 계속 복사됨◎ effectiveFrom 임의 날짜 + 버전 이력
거버넌스/audit△ 셀 편집 권한만 있으면 수정 가능, 근거 고시 메타·변경 이력 약함◎ history 테이블 + updatedBy + 근거 reference + 커밋 시 appliedParameters 스냅샷
수식 가독성△ LOOKUP 키가 "현재 행의 컬럼 값"이라 파라미터별 히든 키 컬럼 필요 (리터럴 키 지원을 추가하면 완화)○ [$statutory.national_pension.employee_rate]

결론 — 역할 분담이 답이다. 이 방식의 최대 강점(가시성·직접 편집)은 정확히 조직 고유 값(임의공제 단가·기숙사비·상품권 등)에 필요한 성질이고, 최대 약점(중앙 갱신 불가·누락 시 0)은 정확히 법정 요율이 감당 못 하는 성질이다. 따라서:

  • 법정 요율·상하한·세액표 → 파라미터 스토어 (전 조직 공통, 벤더 중앙 갱신, 시행일 정밀, 누락=실패). 가시성 요구는 스토어를 읽기전용 "법정 요율 시트 뷰"로 렌더링해 보완 — 파라미터 시트의 UX를 스토어 위에 얹는 것.
  • 조직 고유 단가·임의공제 파라미터 → 파라미터 시트 + LOOKUP (사용자 제안 방식, 단가 시트 선례 그대로).

즉 사용자 제안은 기각이 아니라 적용 대상을 임의공제·조직 파라미터로 특정하는 것이 권고안이다. §7 결정 질문 7로 추가.

4-2. 계산 표면: 4대보험은 수식, 소득세는 조회표 모듈 ​

(a) 4대보험 — 시스템 제공 시트 템플릿의 수식 컬럼이 1급 경로. 상하한 포함 산식이 기존 수식 함수만으로 표현된다:

국민연금 = ROUND(MIN(MAX([보수월액], [$statutory.national_pension.income_floor]),
                       [$statutory.national_pension.income_ceiling])
                 * [$statutory.national_pension.employee_rate] / 100)
  • [$ns.key]는 시트 귀속월 기준 시점 조회가 이미 구현돼 있으므로 과거 월 재계산도 자동으로 당시 요율 — 신규 개발 없이 §1-4 요건 충족.
  • 수식 경로의 결정적 장점: 세무사가 시트에서 산식과 요율 칩을 직접 눈으로 확인할 수 있다(감사 요구와 부합), 셀 단위 override(고지액 차이 보정)도 기존 기능.
  • 각 공제 컬럼에 commitToKind=deduction + 커밋 앵커를 세팅해 "✓ 커밋" 시 원장으로. [확정 — §7 결정 6] 계산 경로는 수식 컬럼으로 일원화한다: insurance codeModule은 폐기 (같은 공제에 두 계산 경로가 있으면 §0-2의 "장기요양 공식 불일치"가 재발한다). 조회표 특성상 수식으로 불가능한 소득세만 전용 모듈을 유지한다(아래 (b)). 사전 검토 결과는 §4-6.

(b) 소득세 — 간이세액표는 수식으로 불가능(수천 행 × 가족수 축), 조회표 모듈로.

  • 신규 테이블(예: simplified_tax_table): 시행일(effectiveFrom) + 급여 구간 + 부양가족 수별 세액. 공공데이터포털/홈택스 Excel을 적재하는 파이프라인(§5)으로 채운다 — importer 앱의 Excel 파싱 경험 재사용.
  • tax-bracket codeModule을 간이세액표 조회 방식으로 교체(현행 연환산 직산은 실제 원천징수와 다름). 직원 데이터에 공제대상 가족 수·자녀 수·원천징수 비율(80/100/120) 필드 신설 필요.
  • 지방소득세는 소득세 × [$statutory.local_income_tax.rate] / 100 수식 + 절사 규칙.

(c) 통지 금액형(ICL·압류·정산분) — Phase 1에서는 시트의 수동 입력 컬럼 + 커밋으로 수용(임의공제와 같은 표면). 분할(splitMonths)·기간 관리는 Phase 3.

4-2b. 산정 기초(보수월액) — 급여대장 연동 설계 (2026-08-19 검토) ​

PoC는 산정 기초를 계약 기본급으로 단순화했지만, 정합성 있는 기초는 급여대장의 최종 수당 합산(비과세 제외) 이다. 기존 시트 연동 구조를 코드로 검토한 결과:

확인된 사실

  • 크로스시트 참조([payroll_ledger:콜키])는 대상 시트를 완전 리졸브한다 — 집계(aggregate) 열 값 포함 (resolver.ts 프리로드가 resolveSheetRows 재귀 호출, 사이클은 얕은 해석으로 절단). 퇴사정산 요약 시트가 자매 시트를 slug-ref로 집계하는 동일 선례가 있다. → 급여대장 수당 열을 공제 시트 수식에서 읽는 것은 기존 메커니즘으로 가능.
  • 그러나 급여대장 템플릿에는 표준 "수당 합계" 열이 없다 — 집계 열은 조직이 수당 항목별로 직접 추가하며 키가 조직마다 다르다. aggregate 열은 앵커 전용(자기 slug+콜키로 커밋된 레코드만 SUM)이라 "이 시트로 커밋된 전 수당 합산" 열도 현재는 만들 수 없다.
  • 비과세 구분 수단이 코드베이스에 전무하다 (taxable/taxFree/비과세 마커 0건) — §1-5의 "급여 항목에 비과세 속성 필요" 요건이 실측으로 확인됨.

설계 제안

  1. 급여대장에 표준 "보험료 기초(과세보수)" 열 신설 — 구현 방식 권고: aggregate의 시트 범위 변형 신설 (aggregate: { recordKind: "allowance", scope: "sheet" } — targetColumnKey 무관하게 해당 시트로 커밋된 전 수당을 SUM, resolver 확장 소규모). 여기에 급여대장 집계 열 메타에 insurableBase: false 플래그(기본 true)를 추가해 비과세 항목(식대·차량유지비 열)을 기초에서 제외한다 — 보험별 포함 예외는 Phase 2+.
  2. 법정공제 시트의 보수월액 = [payroll_ledger:insurable_gross] 크로스시트 참조로 교체. 계약 기본급 열은 참고/폴백으로 유지.
  3. 항목별 산정 기초는 하나가 아니다 — 당월 실보수 기준이 정확한 것은 고용보험(과 건보 정산·소득세)뿐이고, 국민연금은 공단이 확정한 기준소득월액(전년 소득 기반, 매년 7월 갱신), 건강보험 월 고지도 신고 보수월액 기준(실보수와의 차이는 4월 정산)이다. 따라서 정률 모드에서도 항목별로 기초 소스를 선택할 수 있어야 하며(당월 과세보수 / 신고 보수월액 / 공단 고지), 이는 §7 결정 1(모드 선택제)의 열 단위 버전이다. Phase 1은 "당월 과세보수" 단일 기초로 시작하되 이 한계를 명시한다.

4-2c. 과세성(비과세) 플래그의 자리 — 열 메타 vs AllowanceRecord (2026-08-19 검토) ​

전제 확인: 급여대장의 실체는 AllowanceRecord의 합산이며, 주 원천 급여 데이터는 시트가 아니라 커밋 테이블(레코드)이다. 그렇다면 과세성 플래그는 어디에 사는가.

코드로 확인된 사실

  1. 레코드는 마스터가 될 수 없다 — commitSheet는 재커밋마다 해당 시트의 레코드를 sweep(soft-delete) 후 전량 재삽입한다. 레코드에 직접 단 플래그는 다음 커밋에서 증발하므로, 레코드의 플래그는 반드시 "어딘가의 마스터에서 커밋 시점에 복사된 스냅샷"이어야 한다.
  2. 현 아키텍처에서 '급여 항목'의 실체는 급여대장 aggregate 열이다 — eCount A코드도 record FK가 아니라(1816에서 제거) 급여대장 열 메타의 allowanceItemCodeId에 붙어 있고, export는 record의 (targetSheetSlug, targetColumnKey) → 열 메타를 read-time jsonb 조인으로 해석한다 (payroll.router.ts L2052~2105).
  3. 그러나 이 read-time 조인 패턴을 과세성에 쓰면 안 된다 — 열 메타를 나중에 고치면 과거 확정분의 과세성 해석이 소급 변동한다. eCount 코드 매핑은 표시/전송용이라 용인되지만, 과세성은 보험료·세액 금액 계산의 입력이므로 확정 시점에 동결돼야 한다 (§2-4 SAP retro 원칙과 동일).
  4. 레코드에는 이미 커밋 시점 분류 enum(paymentMethod)이 있으나 commitSheet가 스탬프하지 않아 기본값으로만 남는 반쪽 선례 — 커밋 시점 스냅샷 패턴을 이번에 제대로 세울 자리다.

권고: 양쪽 다, 역할을 나눠서 (마스터=열 메타, 스냅샷=레코드)

  • 마스터: 급여대장 aggregate 열 메타에 taxExempt: boolean(기본 false) + taxExemptType(식대/차량유지/보육 등 enum, core enums에) — 항목 설정 UI가 이미 열 설정에 있고, 월별 시트 사본 구조라 과거 월의 과세성이 당시 기준으로 자연 동결된다.
  • 스냅샷: commitSheet가 커밋 시점에 타깃 급여대장 열 메타를 조회해 AllowanceRecord.taxExempt(+type)로 스탬프. 이후 모든 소비자(보험료 기초 합산·간이세액 기초·신고 자료)는 시트 조인 없이 record 쿼리만으로 SUM(amount) WHERE NOT taxExempt를 얻는다 — "레코드가 주 원천"이라는 원칙과 합치.
  • §4-2b의 "보험료 기초" 열(scope:"sheet" aggregate)도 결국 record 쿼리이므로 이 플래그 필터가 그대로 적용된다.

대안 비교 (기각 사유 포함)

위치평가
AllowanceRecord 단독 (마스터)✗ sweep-재생성으로 증발 — 구조적으로 불가
열 메타 단독 + read-time 조인✗ 사후 수정이 확정분 계산 근거를 소급 변경 — audit 위반
별도 항목 마스터 테이블 (AllowanceItemCode 확장/신설)△ 기간 무관 식별자·스키마 검증은 장점이나, record↔itemCode 연결을 1816에서 의도적으로 끊었고 AllowanceItemCode는 eCount 전용 성격. 열 메타 규모가 커지면(보험별 예외 등) 승격 재검토
열 메타(마스터) + 레코드(커밋 스냅샷)✅ 권고 — 기존 커밋 동결 철학(inputSnapshot/appliedParameters)과 동일 패턴

미해결 주의점 — 비과세는 boolean이 아니라 한도부다. 식대는 "월 20만 원까지" 비과세 — 식대 30만 지급 시 20만 비과세 + 10만 과세로 금액이 갈라진다. 항목 단위 플래그(Phase 1)는 "비과세 항목 열은 한도 이내로 운영"이라는 운영 전제 위에서만 정확하며, 한도 초과 자동 분리(레코드의 비과세 인정액 필드 또는 레코드 분리)는 Phase 2 과제로 명시한다.

⚠️ 시스템 템플릿은 한 번 깔리면 갱신되지 않는다 (2026-08-20 실측). 시트 템플릿 시더는 이름이 같은 템플릿이 있으면 통째로 건너뛴다(멱등 삽입). 그래서 PoC가 급여대장 템플릿에 추가한 「보험료 기초(과세보수)」 열은 템플릿이 이미 깔린 조직에 영원히 도달하지 못한다 — 새로 만드는 DB에만 생긴다. dev DB의 급여대장 템플릿·시트 어디에도 그 열이 없었다.

그대로 두면 법정공제 시트가 보수월액을 구하지 못하고, 커밋 차단 게이트가 전 조직에서 영구히 커밋을 막는다 — 게이트는 기초가 "아직 없는" 상태를 막으라고 만든 것이지, 열이 없어서 영원히 못 만드는 상태를 막으라고 만든 게 아니다. 마이그레이션 1887로 템플릿과 기존 급여대장 시트 양쪽을 보강했다(시트는 생성 시점에 레이아웃을 복사하므로 템플릿만 고치면 기존 시트는 비어 있다).

앞으로 시스템 템플릿에 열을 추가할 때는 시더만 고치면 안 되고 마이그레이션을 함께 써야 한다.

정합성 가드 (필수)

  • 참조는 수당 열만 — 실지급액(수당−공제) 류 열을 기초로 잡으면 record-level 순환(공제 커밋 → 실지급 변동 → 공제 재계산)이 생기며, 수식 사이클 절단은 formula-level만 끊는다. 수당 합산만 참조하면 한 번에 수렴(공제는 수당에 영향 없음).
  • 급여대장 부재/미커밋 시 조용한 0 방지 — ✅ 해소 (T6). 미해결 크로스시트 참조는 0으로 평가되므로 보수월액 0 → 하한 클램프 → 최소 보험료(국민연금 19,475원)가 조용히 계산된다. 계약 기본급 폴백을 제거해 기초를 급여대장 하나로 통일하고, 「보험료 기초 없음」 열을 커밋 차단 조건으로 걸었다.
    • 폴백이 만든 실제 결함: 4대보험은 계약 기본급(폴백)으로, 소득세는 수당 레코드 합산으로 계산돼 한 사람의 한 달 급여가 두 기준으로 나왔다(실측 2026-08 — 4대보험 300만 기준 / 소득세 0 기준).
    • ⚠️ 구현 함정: 이 가드 열을 boolean 포맷으로 두면 무력화된다. 급여대장이 없으면 크로스시트 참조가 미해결이라 수식 전체가 N/A가 되고, 정작 잡아야 할 "기초 없음"에서만 판정이 사라진다. 숫자 포맷 열로 받아야 한다(mjCode·judgeIncompleteNum과 같은 이유). 실측으로 한 번 물렸다.
  • 커밋 순서와 신선도 — 운영 순서는 "수당 시트 전부 커밋 → 급여대장 수당 확정 → 법정공제 계산·커밋". 공제 커밋 후 수당이 재커밋되면 공제 레코드가 stale — 커밋 스탬프(migration 1827)를 활용한 stale 감지/재커밋 유도 필요.

4-3. 판정 매트릭스 (Layer 1의 구현) ​

  • 월 법정공제 시트 생성 시 재직자 전원을 auto-populate (기존 setAutoPopulate 활용). — ✅ 구현
  • 항목별 "적용/제외/사유" 판정 컬럼을 둔다. 판정 로직(연령·고용형태·시간 기준)은 코드가 계산하고, 결과는 시트에 가시화 — 판정이 틀렸으면 세무사·관리자가 시트에서 바로 본다. — 🟡 판정 열은 구현, 사유 기록과 표현은 미완(아래).
  • 커밋 게이트에 "법정공제 시트는 판정 미수행 행이 있으면 커밋 불가" 규칙을 추가. — 🟡 구현하되 정책이 바뀌었다 (T1 → 확정 ①). 2026-08-20 에는 커밋을 거부했으나, 2026-08-21 기획 확정으로 막지 않고 확인받는다. 템플릿의 「판정 불가」 열(숫자 포맷 judgeIncompleteNum — 생년월일·입사일·주 소정근로시간 결측)을 공제 열 메타 commitWarnings 가 가리키고, commitSheet 가 sweep 이전에 해당 행을 세어 첫 호출을 되묻는다(직원 이름·사번까지 안내). 사용자가 확인하면 진행하고 건수를 리포트에 남긴다. ⚠️ 옛 이름 judgmentGuardColumn 은 commitBlockers(차단)/commitWarnings(경고)로 갈라졌다. 판정 불가의 정의는 시트 수식이 갖는다 — 항목마다 필요한 근거가 다르기 때문(소득세는 부양가족 수라 이 경고에 아직 포함되지 않는다).
    • 다만 이 문장은 대상을 잘못 지목했다: apps/hr-system/src/sheets/workbook/commit-gate.ts는 열 설정을 저장할 때 그 열의 반영 대상을 남길지 버릴지 판정하는 클라이언트 순수 함수이며, 행 단위 판정과 무관하다. 필요한 게이트는 열 반영 게이트(열 설정 드로어)와 행 판정 게이트(서버 commitSheet, 파라미터 게이트 옆)로 서로 다른 자리에 있다.

구현 결과 (§6-4 ①) — 판정은 시트 수식 열로 들어왔다: 숨김 입력 4열(생년월일·입사일·주 소정근로시간·급여 마감일) + 판정 5열(연령·초단시간·연금 제외·건보 제외·고용 제외) + 공제 수식의 게이트(IF([npsExcluded], 0, …)).

남은 갭 4건

#갭왜 문제인가
1판정 열이 영문 True/False로 표시급여 시트에 불리언 5열이 늘어서고, True가 파란 굵은 글씨라 긍정으로 읽힌다 — 실제 뜻은 「이 사람은 이 보험에서 빠짐」 (workbook-adapter.ts L2240)
2경고를 데이터 열로 표현「⚠ 기본급 폴백」이 다른 판정 열과 똑같은 파란 True로 뜬다. 경고는 열이 아니라 상태여야 한다
3판정 근거가 숨김근거 4열이 visible: false라 「연금 제외 True」의 이유를 화면에서 볼 수 없다 — 세무사 audit 요구와 충돌
4사유가 원장에 없음✅ 해소 (T2) — 커밋 시점에 DeductionRecord.exclusionReason 으로 동결한다 (§4-4)

1~3의 표시 형식은 §4-7, 4는 §4-4(원장 보강)에서 다룬다.

4-4. 원장 보강 (audit 요건) ​

  • deductionType(법정 6종 + 상황부 + 임의) 구분을 원장에 추가 — 정의만 있는 DeductionSubCategory enum을 확장·실사용하거나 신규 enum. 현재는 reason 문자열뿐이라 audit 집계가 불가능하다. — ❌ 미구현
  • commitSheet가 sourceType·formulaVersion을 기록하도록 보완, appliedParameters 스냅샷에 파라미터 버전 id까지 포함 → 공제액마다 "어떤 규칙 버전으로 계산됐나"를 재현 가능하게 (rule lineage). — ✅ 구현 (§6-3·§6-1)
  • 판정 결과(적용/제외 + 사유)도 커밋 시점에 레코드로 동결한다. — ✅ 구현 (T2, migration 1886). 과세성 스냅샷(결정 8)과 같은 패턴이다: 시트 판정 열이 마스터, DeductionRecord.exclusionReason 이 커밋 시점 스냅샷. 어느 사유인지는 공제 열 메타의 exclusionReasons(사유별 판정 열 매핑, 배열 순서 = 우선순위)가 정한다. NULL = 적용이고, 값이 있으면 제외이며 그때 amount 는 0 이다 — 0 원 레코드도 커밋되므로 제외 사실이 원장에 남는다.
    • ⚠️ 수식 엔진의 함수 테이블은 (args: number[]) => number 라 수식이 사유 문자열을 직접 낼 수 없다. 그래서 사유마다 판정 열(age60 = =[age] >= 60, hire65, shortTime)을 두고 서버가 첫 참을 채택한다. npsExcluded/uiExcluded 는 이 원자 열들의 OR 로만 표현해 판정 로직의 출처를 하나로 유지한다.
  • audit 패키지 export(§3 Layer 5)는 이 필드들의 조회·리포트다. — ❌ 미구현

4-5. 선결 수리 목록 — 기획 확정과 무관하게 고쳐야 할 것 ​

#수리위치현황 (2026-08-20)
1computed 컬럼 모듈 키 불일치 ("insurance"/"tax_bracket" → enum 값) + "미지원 모듈은 조용히 skip" 제거(에러화)packages/api/src/routers/sheet-rows/compute.ts L231·L579·L660✅ 완료 (§6-1)
2commitSheet의 sourceType/formulaVersion 미기록같은 파일 L1124~✅ 완료 (§6-1)
3연말정산·퇴사정산 → 공제 반영이 lineage NOT NULL 위반으로 실패year-end-settlement.router.ts L290~, resignation-record.router.ts L378~❌ 미완
4TaxBracketCalculator가 귀속연도 아닌 현재연도 로드tax-bracket.calculator.ts L34✅ 해소 — 다만 "고쳤다"기보다 구 누진 직산 로드를 통째로 제거했다. 어댑터는 module-registry 메타데이터 전용 스텁이 됐고, 실제 소득세 경로는 sheet compute다
5장기요양 공식 이원화(건보료 대비 vs 급여 대비) 단일화 + seed 템플릿의 하드코딩 요율을 [$statutory.*] 참조로 교체insurance.ts vs seed-sheet-templates.ts L1802~🟡 부분 — 퇴사정산 템플릿에 2024년 요율이 잔존(L2190·L2199). insurance.ts 모듈도 아직 삭제되지 않음(결정 6 = 폐기)
6codeModule 파라미터의 $: 해석이 live 값만 조회(시점 미고려) — 수식 경로와 시점 정책 일원화compute.ts L46~108✅ 완료 (§6-3 resolveOrgConstantValues 공유)

4-6. codeModule 사용 실태 — 결정 6의 사전 검토 결과 (2026-08-19) ​

  • 레지스트리에 모듈 9개. loadInputsByModule 분기 키와 enum 값이 일치하는 7개(초과근무·근로계약·고정인센티브·교육비·퇴사·연말정산·manual)는 수당 쪽에서 정상 사용 중 — 레지스트리 + 입력 로더 + 파라미터 스키마 아키텍처 자체는 건전하며, 전면 리팩토링은 불필요하다.
  • 죽어 있는 것은 정확히 공제 쪽 2개(insurance/tax-bracket — §0-2의 키 불일치)와, 존재하지 않는 모듈을 참조하는 seed 템플릿 컬럼 3개(treatment_daily — 레지스트리·enum 어디에도 없음 → 해당 컬럼은 조용히 미계산. seed-sheet-templates.ts L860·870·880).
  • 조치: ① insurance 모듈 폐기(수식 일원화), ② tax-bracket 모듈은 간이세액표 조회 방식으로 교체, ③ "미지원 모듈은 조용히 skip"(compute.ts L660)을 에러로 승격 — treatment_daily 같은 유령 참조도 이 조치로 드러난다, ④ $: 시점 정합화(§4-5 #6). 수당 쪽 7개 모듈은 손대지 않는다.

4-7. 상태 표시 요구사항 — 발견 장치의 화면 규격 (2026-08-20) ​

§3 [개정]이 차단을 포기한 대가로 요구되는 것. 지금까지 이 문서는 계산·저장만 규정했고 화면은 세 문장(§0-2 "UI에서는 모듈 선택이 가능하게 보임", Layer 2 "관리자에게 알림", §4-3 "시트에 가시화")뿐이었다. 그 공백을 여기서 규격으로 만든다. 세부 시각 설계는 별도 디자인 작업(브랜치 design/statutory-deduction-states)에서 이어진다.

원칙 2

  1. 색은 심각도, 형태와 행동은 책임 주체. 현행 .ds-notice 4종(info·warning·success·danger)은 심각도 축만 있어 hereby가 처리 중인 것과 조직이 입력해야 하는 것이 같은 모양으로 뜬다. 색을 늘리는 대신 이동 경로(CTA) 유무를 규칙으로 고정한다 — 조직이 고칠 수 있으면 반드시 달고, hereby 소관이면 절대 달지 않는다. 커밋 거부 문구가 결정 3과 어긋난 것(§3 Layer 2 현황)이 이 규칙이 없어서 생긴 사고다.
  2. 실패·미판정을 숫자 자리에 숫자로 쓰지 않는다. 공제 0원은 제외 판정된 직원의 정당한 값이므로, 0과 빈칸은 정상에만 쓴다. 현재 「연령 0」·「소득세 빈칸」이 이 원칙을 어기고 있다.

표시할 상태 (5)

상태뜻고치는 주체반영(커밋)
판정 불가적용/제외를 정할 근거가 없음 (생년월일·근로시간 결측)조직 관리자허용(fail-safe) + 집계 노출
계산 실패규칙은 있으나 값이 안 나옴 (요율·표 부재)hereby거부 (§6-5 ②)
입력 필요조직이 채울 값이 비어 멈춤 (부양가족 수, 고지서)조직 관리자항목별 판단
외부 확정 대기공단·국세청 확정이 있어야 정해짐 (이중근로 안분, 정산분)없음허용 + 잠정 표식
사전 경고아직 실패 아님, 곧 실패 (내년 요율 미입력, 상류 stale)hereby / 조직허용

표시 자리 (5계층) — 아래로 갈수록 구체적, 위로 갈수록 집계. 같은 문장을 다섯 번 반복하지 않는다.

계층무엇을 말하나
셀이 값이 왜 없는지 — 상태어(숫자 아님)
행이 직원에게 몇 건 — 모아 보기 대상
열 헤더항목 전체에 걸친 규칙 문제 (요율 부재는 전 직원 동시 발생이므로 셀마다 칠하지 않는다)
시트 배너 · 반영 버튼집계와 거부 사유
알림마감까지 남은 시간, 조직 밖에서 온 변화 (요율 개정·표 갱신)

확정 필요 — 디자인 착수 전 기획 확인 사항은 §7 미확정 질문 참조.


5. 운영 프로세스 — 요율 개정을 반영하는 워크플로 ​

기술이 아니라 운영이 절반이다. 요율 공식 API는 사실상 없으므로(간이세액표만 파일 제공), 사람이 개입하는 반자동 파이프라인을 정식 프로세스로 설계한다.

[감지]     고시·개정 모니터링 — 연간 캘린더(1월 요율 / 2~3월 간이세액표 / 7월 국민연금 상하한) + 비정기 감시
[스테이징]  새 파라미터 버전 입력 (시행일 + 근거 고시 URL) — 아직 미적용
[검증]     golden test 자동 재실행 + 영향 시뮬레이션 (직원별 공제액 변동 미리보기)
[승인]     선택적 승인 — 승인 없이도 활성화 가능하되, 릴리스마다 승인 여부·승인자·시점이 기록·조회 가능
[릴리스]   버전 번호 + 릴리스 노트 ("2027-01 요율 패키지: 건보 7.19→x.xx%, 근거 고시 …")
[알림]     조직 관리자에게 변경 알림 (기존 notification 시스템 재사용)
  • [확정 — §7 결정 3] 파라미터 관리 주체는 hereby 팀 (벤더 책임 모델 — 한국 시장 표준 기대치). 세무사 승인은 필수가 아니되, audit 과정을 기록할 수 있는 경로를 제공한다: 각 파라미터 릴리스에 승인 상태(미승인/승인, 승인자, 시점)가 남고 audit 패키지(§3 Layer 5)에서 항상 확인 가능하다.
  • 간이세액표는 "공공데이터포털/홈택스 파일 파싱 → 스테이징 → diff 미리보기 → 승인 → 반영"의 동일 파이프라인.
  • [검증] 단계의 실행 절차는 별도 문서로 확정 — statutory-deduction-release-validation.md: 홈택스/공단 계산기와의 표본 대조(경계 배치 23표본 + 4대보험 4표본), 대조 러너 verify-tax-samples.ts(전건 일치 = 종료 코드 0 — 릴리스 게이트에 직결 가능), 불일치 분류(변환/산식/개정 시차), 결과 기록 규칙.

6. 단계별 로드맵 (안) ​

진행 상황 (2026-08-20). PoC가 로드맵을 앞질렀다 — Phase 2(소득세)가 Phase 1보다 먼저 사실상 끝났고, Phase 3의 일부(비과세 한도 초과 분리)도 앞당겨졌다. 반대로 Phase 1의 모드 선택·상태 표시·병행운영은 손도 대지 않았다. 남은 것은 "계산"이 아니라 운영과 화면이다.

Phase내용진행
0. 기반 수리 + 요율 스토어§4-5 수리 6건(+§4-6 조치) · statutory.* namespace 확정 · 글로벌 기본값 계층 · 2026 요율 + 근거 메타 seed · "유효값 없으면 실패" 정책 · 법적 책임 구조 법률 검토(결정 4)🟡 대부분 완료 — 잔여: 수리 3·5, insurance.ts 폐기, 글로벌 계층(일괄 갱신으로 대체 — 결정 5), 법률 검토는 질의서 작성까지(회신 대기, 여전히 Phase 1 착수의 게이트)
1. 4대보험 월 공제법정공제 레지스트리 + 판정 매트릭스 · 시스템 시트 템플릿(수식+상하한+커밋 앵커) · 공제 산정 모드 선택 골격(정률 계산/고지 기반/수동/외부 import — 결정 1·2) · eCount import 파서 완성(외부 계산 모드) · golden test 1차 세트🟡 계산·판정·커밋·golden test 완료. 미착수: 모드 선택제 · eCount 파서 · 상태 표시(§4-7) · 병행운영 절차
2. 소득세·지방소득세간이세액표 테이블 + 파일 적재 파이프라인 · 부양가족/원천징수비율 직원 필드 · 조회 모듈 교체✅ 대부분 완료 (§6-6·§6-7). 단 부양가족 수·자녀 수·원천징수비율이 직원 필드가 아니라 시트 리터럴 열로 구현됐다 → 미확정 질문 ②
3. 정산·통지형·대사고지액 업로드 + 자동 대사(Layer 4) · 건보 4월 정산 · ICL/압류 (분할 관리) · 휴직 일할계산❌ 미착수. 단 비과세 한도 초과 분리는 앞당겨 완료 (§6-5 ①)
4. 세무 시스템 확장원천세 신고 자료 생성 · 지급명세서 · 연말정산별도 기획 · 추후 예정

6-1. PoC 구현 결과 (2026-08-19, 브랜치 feat/statutory-deduction-poc) ​

방향 확인용 수직 슬라이스를 구현·검증했다. 커밋 533dbad5.

구현한 것

  • statutory.* 요율 파라미터 시드 7종 — 시행일별 history 버전 + 근거 메타 (seed-statutory-parameters.ts)
  • 시스템 템플릿 「법정공제 — 4대보험 (월별)」 — 재직자 1인 1행(컬럼 sourceTable: "employee" 바인딩, 급여대장과 동일 방식), 수식이 [$statutory.*] 참조 + MIN/MAX 상하한 클램프, 공제 4열은 커밋 앵커(payroll_ledger/ded_statutory_*) 장착
  • §4-5 수리 #1(모듈 키 불일치)·#2 일부(commitSheet sourceType 기록 + priorDeductions NULL 필터 수리)·조용한 skip에 warn 로그

검증된 것 (전부 API 레벨 E2E)

  • 템플릿 적용 → 재직자 17행 자동 생성, 수식 계산값이 기대값과 전 행 일치
  • 시점 정합: 2026-08 시트는 2026 요율(4.75/3.595/13.14), 2025-12 시트는 자동으로 2025 요율(4.5/3.545/12.95) — 신규 개발 없이 §1-4 요건 충족 확인
  • ✓ 커밋 → DeductionRecord 68건(17명×4항목), sourceType·타깃 앵커 정상

PoC가 드러낸 갭 (Phase 0~1 과제로 승격)

  1. 수식 커밋의 rule lineage 부재 — inputSnapshot에 수식 문자열만 남고 그때 사용된 [$statutory.*] 값은 안 남는다 (appliedParameters는 codeModule 경로 전용). 감사 재현성을 위해 resolver가 사용한 org constant 값을 커밋 스냅샷에 포함해야 한다.
  2. 판정 매트릭스 필요성의 실증 — 계약 없는(보수 0) 직원에게 하한 클램프가 작동해 국민연금 19,475원이 계산됐다. "적용/제외 판정 없이 계산부터 하는" 구조의 실제 오류 사례 (§3 Layer 1).
  3. [$...] 미해결 시 여전히 N/A/0으로 조용히 통과 — "유효값 없으면 실패" 정책(Layer 2)은 미구현.
  4. PoC 단순화 잔여: 산정 기초=계약 기본급(→ §6-2에서 해소), 건보 상하한 미적용, 절사 단순 ROUND.

6-2. PoC 2차 — 산정 기초의 급여대장 연동 + 과세성 스냅샷 (2026-08-19) ​

§4-2b·§4-2c 확정 설계(결정 8)를 같은 브랜치에 구현·검증했다.

구현한 것

  • TaxExemptType enum(core) + 급여대장 aggregate 열 메타 taxExempt/taxExemptType(마스터) + AllowanceRecord.taxExempt/taxExemptType(스냅샷, migration 1883)
  • commitSheet가 커밋 시점에 타깃 급여대장 열 메타를 읽어 수당 레코드에 과세성을 동결 스탬프
  • aggregate scope:"sheet" 변형 — "이 시트로 커밋된 전 수당 SUM, 비과세 제외" (excludeTaxExempt)
  • 급여대장 템플릿에 표준 열 insurable_gross 「보험료 기초(과세보수)」 추가; 법정공제 템플릿의 보수월액을 [payroll_ledger:insurable_gross] 크로스시트 참조로 교체 (미커밋 시 계약 기본급 폴백 — Phase 1 목표는 명시적 실패)

E2E 검증 (2026-09 신규 기간, 전부 통과)

  • 수당 시트에서 기본급 300만/700만 + 식대 20만 커밋 → 식대 레코드에 taxExempt=true, type=meal 스탬프 확인
  • 급여대장 insurable_gross = 300만/700만 — 식대 20만이 기초에서 제외됨
  • 법정공제 시트 보수월액이 급여대장 값으로 채워지고, 700만 직원은 국민연금 상한(659만) 클램프 후 313,025원 — 전 항목 기대값 일치
  • 수당 미커밋 직원은 계약 기본급 폴백 동작

후속 과제

  • 과세성 플래그 편집 UI → 완료 (§6-3)
  • 신규 시트의 설정 라이프사이클(잠금)과 커밋 순서 UX — E2E에서 reopenSetup/completeSetup 수동 호출이 필요했음
  • 폴백 → 명시적 실패/경고 전환 → ✅ 완료 (T6, §4-2b 가드 2). 비과세 한도 초과 분리(§4-2c) → ✅ 완료 (§6-5 ①)

6-3. PoC 3차 — rule lineage + 과세성 편집 UI (2026-08-19) ​

  • rule lineage (§6-1 갭 1 해소): 수식 커밋 시 시트의 수식들이 참조한 [$ns.key] 상수를 귀속월 기준으로 해석해 레코드의 appliedParameters로 동결한다 — 시점 해석 로직은 화면 평가와 같은 함수(resolveOrgConstantValues, resolver에서 추출·공유)를 쓴다. 검증: 2026-09 국민연금 313,025원 레코드에 statutory.* 요율·상하한 6종의 당시 값이 스냅샷으로 남음 — "이 공제액이 어떤 규칙 버전으로 계산됐나"를 레코드만으로 재현 가능.
  • 과세성 편집 UI: hr-system 열 설정 패널의 집계(수당) 섹션에 「비과세 항목」 체크박스 + 유형 select 추가 (column-settings-panel.html). 저장은 열 메타 taxExempt/taxExemptType(과세는 필드 미기록), 안내 문구로 "커밋 시점 동결 — 변경 후 수당 시트 재커밋 필요"를 명시. preflight(Fast) 통과.

6-4. PoC 4차 — 판정 매트릭스 · 신선도 감지 · 간이세액표 · 법률 질의서 (2026-08-19) ​

① 판정 매트릭스 (Layer 1, §4-3 구현) — 법정공제 템플릿에 판정 클러스터 추가: 숨김 identity 입력(생년월일·입사일·주 소정근로시간·급여 마감일) + 수식 판정 열(연령·초단시간·연금/건보/고용 제외) + 공제 수식 게이트(IF([npsExcluded], 0, …)). 설계 원칙 그대로 fail-safe: 제외가 명시적으로 참일 때만 0, 판정 입력 부재 시 공제는 계산되고 이상값(연령 0)이 화면에 드러난다. 연령 기준일은 TODAY가 아닌 payEndDate라 과거 월 재계산에도 정확. E2E: 63세 직원 연금만 0원, 게이트 일관성 17/17. 실측 버그 2건 발견·수정: (a) 날짜 직렬값이 1970 기준이라 1970년 이전 출생의 > 0 존재 검사가 거짓 — 부호 무관 OR(>0,<0)로 교체.

② 상류 신선도 감지 (§4-2b 가드 3) — commitStatus 응답에 upstreamAllowanceLatestAt/staleAgainstAllowances 추가: 공제 커밋 후 수당이 재커밋되면 stale로 표시. E2E: 커밋(false) → 수당 수정·재커밋(true) → 공제 재커밋(false) 라이프사이클 확인. UI 배지 노출은 후속.

③ 간이세액표 (Phase 2 선행) — simplified_tax_tables(effectiveFrom 버전 × 구간 × 가족수, migration 1884) + CSV 적재 파이프라인(import-simplified-tax-table.ts, 버전 단위 적재·부분 갱신 거부) + tax 모듈 재작성(과세보수=수당합산−비과세 스냅샷, 시트 리터럴 입력 부양가족수·원천징수비율, 조견표 조회, 소액부징수·10원 절사 — ⚠재확인 표기). 표/구간 없으면 셀을 비워 두고 warn — 조용한 0 방지가 여기도 관철됨(커밋도 스킵). 템플릿에 소득세·지방소득세 열 추가. E2E: 84,850×120%=101,820 / dep4 420,000 / 지방세 10원 절사 / 커밋 sourceType=tax_bracket_config. 실측 버그: (b) date 컬럼 SELECT 시 pg 드라이버의 로컬 TZ Date 파싱으로 버전 등호 매칭이 하루 어긋남 — to_char 문자열 고정으로 수정. ⚠ 실제 간이세액표 데이터는 미적재 — 공공데이터포털 파일을 CSV 변환해 적재 + 홈택스 표본 대조(Layer 3)가 상용 전 필수.

④ 법률 검토 질의서 — docs/internal/legal-review-questions-statutory-deduction.md (결정 4의 선행 과제): 계산 오류 책임 경계, 모드별 책임, 파라미터 중앙 관리·선택적 승인의 효력, 세무사법상 세무대리 해당 여부, 개인정보(부양가족 수·주민번호), 기록 보존, 통지형 공제 — 7개 쟁점 + 요청 산출물 정리.

남은 후속 — 실표 적재+golden test, stale/폴백 UI 배지, 커밋 순서 UX, 비과세 한도 초과 분리, 글로벌 파라미터 계층, [$...]·기초 0의 명시적 실패 정책 → §6-5에서 대부분 해소.

6-5. PoC 5차 — 후속 일괄 처리 (2026-08-19) ​

① 비과세 한도 초과 분리 (§4-2c 해소) — 열 메타 taxExemptLimit(비과세 월 한도) + AllowanceRecord.taxExemptAmount(비과세 인정액, 커밋 시점 min(금액,한도) 동결, migration 1885) 신설. 기초 합산이 boolean 필터에서 SUM(amount − taxExemptAmount) 금액 차감으로 바뀌어 한도 초과분이 자동으로 과세 기초에 포함된다. 열 설정 UI에 한도 입력(식대/자가운전 선택 시 20만 자동 프리필). E2E: 식대 30만 커밋 → 인정액 20만 스탬프, 보험료 기초 = 기본급 300만 + 초과분 10만 = 310만.

② [$…] 명시적 실패 승격 (Layer 2 완성) — 커밋 시 수식이 참조한 상수 중 귀속월 유효값이 없는 것이 있으면 커밋을 거부한다 ("귀속월(YYYY-MM)에 유효한 파라미터가 없어 커밋할 수 없어요: …"). 게이트는 sweep 이전에 서서 기존 확정분을 건드리지 않음을 검증. 화면 표시는 기존 N/A 유지 — 확정만 막는다.

③ stale UI 배지 (커밋 순서 UX) — 시트 헤더 커밋 칩에 「수당 변경됨 — 재반영 필요」 빨간 라벨 + 툴팁 조치 안내 (staleAgainstAllowances 소비). 기존 「반영 필요/반영됨」 배지보다 우선.

④ 전 조직 일괄 갱신 + 신규 조직 자동 시드 (글로벌 계층 대체) — scripts/update-statutory-parameter.ts: 개정 1회 실행으로 전 조직에 시행일 버전 + 근거 고시를 릴리스 (멱등). 워크스페이스 프로비저닝이 seedStatutoryParameters를 자동 호출해 신규 조직에도 요율이 깔린다. 진짜 글로벌 기본값 테이블은 계속 후속(운영상 필요성 낮아짐).

⑤ golden test (Layer 3 착수) — statutory-formula.golden.test.ts (19 케이스): 실제 템플릿 수식을 evaluateFormula로 직접 실행 — 상하한 클램프·판정 게이트·1970 이전 날짜 가드·fail-safe(null 판정 시 공제 유지)·폴백, tax 모듈의 절사·소액부징수·80/120% 비율·"표 없으면 출력 없음". 수식 사본은 템플릿과 의도적 동거(변경 시 테스트가 깨져 리뷰에 드러남). ⚠ "참값" 검증(국세청 공표값 대조)은 실표 적재와 함께 별도.

진짜 남은 것 — 실표(간이세액표) 적재 → §6-6에서 완료.

6-6. PoC 6차 — 실표 적재 + 자녀 공제 차감 (2026-08-19) ​

① 실표 적재 완료. 공공데이터포털 「국세청_근로소득 간이세액표_20240229」의 공식 HWPX(소득세법 시행령 별표2 <개정 2024.2.29>)를 내려받아 OWPML 표를 파싱 → 646구간 × 가족수 11 = 7,106행 + 최저구간 미만 0원 규칙행 11행 = 7,117행 CSV로 변환, 리포에 보존(packages/database/data/simplified-tax-table-20240229.csv + 출처·검증 README). 변환 검증: 구간 연속성(빈틈 0)·소득 단조성·가족수 감소 전수 통과, 표본 대조 월 300만/1인 = 74,350원(공표값 일치). dev DB의 HEREBY_DEMO·DEMO_HOSP에 적재(effectiveFrom 2024-02-29), 픽스처 제거.

② 확인된 도메인 사실 — 2026-03-01 개정은 본표가 아니라 자녀 차감액이다. 별표2 비고 3: 8~20세 자녀가 있으면 조견표 세액에서 자녀수별 금액을 차감(음수→0). 2024판 12,500/29,160/+25,000 (공식 문서 원문) → 2026-03-01 지급분부터 20,830/45,830/+33,330 (자녀세액공제 연 25/30/40만의 월할 — 복수 출처 교차 확인). 본표(월급여×가족수)는 2024-02-29판이 계속 유효.

③ 자녀 공제 차감 엔진 구현. 차감액을 시행일별 파라미터 statutory.income_tax.child1_deduction/child2_deduction/child_extra_deduction(2024·2026 두 버전 시드)로 관리 — 귀속월 기준 자동 선택되므로 2026-02 시트는 구액, 2026-03 이후는 신액. 템플릿에 childrenCount(8~20세 자녀수) 리터럴 열 추가, 모듈이 "조견표 세액 − 자녀 차감(음수→0) → 80/100/120% → 절사·소액부징수" 순으로 계산. golden test 24케이스로 확장.

E2E (실표, 전부 일치): 3.1M/1인/120% → 82,900×1.2 = 99,480 / 지방세 9,940; 7M/4인/자녀2 → 527,350 − 45,830 = 481,520 / 48,150. 커밋 스냅샷에 표값·자녀수·차감액 lineage 동결. 소득 0원 직원도 규칙행(0~770천원=0)에 매칭돼 스킵 0건.

한계 (문서화): 표는 월급여 1천만 원까지 — 초과분 미지원 → 초과 산식 구현 완료 (§6-7). 상용 적재 시 조직별 실행 필요(현재 dev 2개 조직), 홈택스 모의계산과의 표본 대조를 §5 릴리스 절차에 포함할 것.

6-7. 월급여 1,000만 원 초과 — 별표2 하단 산식 구현 (2026-08-20) ​

표가 끝나는 1,000만 원 초과 구간은 별표2 하단의 법정 산식이다: 세액 = "10,000천원인 경우의 해당 세액"(가족수별 전용 행 — 1인 1,507,400원) + 구간별 가산액(10~14M: 초과분×98%의 35%+25,000 / 14~28M: 1,397,000+초과분×98%의 38% / 28~30M / 30~45M / 45~87M / 87M 초과: 31,034,600+초과분의 45%). 공식 문서에서 산식 원문을 추출해 구현했다.

  • 전용 행 11개(가족수별)를 표에 개방 상단 행으로 추가(CSV 7,128행), 어댑터가 초과 케이스에서 기저값으로 조회.
  • 산식 상수는 모듈 overCapAddon에 개정 주기 경고 주석과 함께 — 표 재적재 시 산식도 함께 대조하도록 data README 체크리스트화.
  • 자녀 차감·80/100/120%·절사·소액부징수가 산식 결과에도 동일 순서로 적용.
  • golden test 30케이스 (경계 14M에서 가산식이 다음 구간 상수 1,397,000과 정확히 이어지는 산식 자체 정합성 포함). E2E: 12.1M/1인/120% → 2,703,240 / 지방세 270,320 정확 일치.

7. 결정 사항 — 대안 비교와 확정 결론 ​

아래 대안 비교를 배경으로, 2026-08-19 결정 7건이 확정됐다 (§7 말미 표).

방향내용장점단점/리스크
A. 자체 계산 엔진 (이 문서의 주안)파라미터 스토어 + sheet 수식/모듈로 전 항목 자체 계산제품 경쟁력, 세무 시스템 확장의 토대, 설명가능성규칙 유지 운영 부담이 hereby에 귀속, 법적 책임 설계 필요
B. 고지액 중심4대보험은 공단 고지서 업로드가 1급 경로, 계산은 검증용 보조법적으로 안전(진실 원천=고지액), 구현 작음소득세는 여전히 계산 필요, 자동화 가치 반감
C. 외부 검수 결합A/B + 세무사·회계법인 파트너가 승인·신고 수행 (뉴플로이 모델)책임 분산, audit 요구를 파트너십으로 해결파트너 확보·수익 배분, 제품 단독 완결성 저하
D. eCount 위임 (현재 코드의 스캐폴드)eCount 급여모듈이 공제 계산 → xlsx를 hereby로 import→커밋계산·컴플라이언스 책임이 eCount에수작업 왕복(Open API 미제공), 자동화·제품 가치 없음, audit 근거도 외부. A를 하면 D는 대체됨

확정 결정 (2026-08-19) ​

#질문결정
14대보험의 1급 경로조직별 모드 선택제 — 정률 계산 / 고지 기반 / 수동. 어느 모드든 Layer 4 대사가 양쪽을 잇는다
2eCount 왕복 스캐폴드대체하지 않고 요구사항으로 같이 제공 — 모드의 하나(외부 계산 import)로 유지하고 파서를 완성한다
3파라미터 관리 주체hereby(회사) 전담. 세무사 승인은 필수가 아니되, audit 과정을 기록하는 경로를 제공 — 릴리스마다 승인 여부·승인자·시점이 기록되고 항상 확인 가능 (§5)
4계산 오류 시 법적 책임개발 착수 전 법률 검토 리서치 선행 — 이용약관·면책·모드별 책임 경계(자체 계산 vs 고지 vs 외부 import). Phase 0에 편성, Phase 1 착수의 게이트
5대상 범위전 조직 기능 (단일 조직 파일럿 아님). 글로벌 기본값 계층(§4-1)이 전제 조건 → [갱신 2026-08-20] 전제 조건을 전 조직 일괄 갱신 스크립트 + 신규 조직 자동 시드(§6-5 ④)로 대체했다. 글로벌 기본값 테이블 자체는 후속 과제로 남되 착수의 전제는 아니다
6계산 경로수식 컬럼으로 일원화 — insurance codeModule 폐기, 조회표형(간이세액표)만 전용 모듈 유지. 사전 검토 결과(§4-6): 수당 쪽 7개 모듈은 정상 사용 중, 전면 리팩토링 불필요
7요율 저장 위치역할 분담 — 법정 = 파라미터 스토어, 조직 고유 = 파라미터 시트 (§4-1b)
8과세성(비과세) 플래그 위치마스터 = 급여대장 aggregate 열 메타 / 사용값 = AllowanceRecord 커밋 시점 스냅샷 (§4-2c). 산정 기초는 급여대장 「보험료 기초(과세보수)」 열(§4-2b)

확정 결정 (2026-08-21) — §7 미확정 질문 6건 회신 ​

2026-08-20 에 제기한 질문에 대한 확정 답이다. 브랜치 design/statutory-deduction-states (PR #581)에 구현·검증까지 반영했다.

#질문확정구현
①판정 불가 행을 반영에서 어떻게 다루나기획서 제안대로 — 막지 않고, 반영 직전에 「판정 불가 N명」을 확인받고 결과를 리포트에 남긴다✅ 열 메타 commitWarnings + acknowledgeWarnings 2단계. 직전 구현(차단)을 뒤집었다. 기초 부재는 그대로 차단 — 넘어가면 금액이 틀리기 때문(하한 클램프)
②부양가족 수·자녀 수·원천징수비율의 자리현행 유지 (시트 리터럴 열). 시트를 볼 수 있는 사람은 이미 직원 정보에 접근할 수 있는 사람이므로 개인정보 쟁점이 성립하지 않는다✅ 변경 없음. ⚠️ 리뷰어 확인 요망 — 기획 초안의 "직원 필드로 승격" 제안을 채택하지 않은 결정이다. 매월 재입력이 필요한 점은 감수한다
③자동 판정을 사람이 뒤집을 수 있게 하나허용하되 사유 입력을 강제하고, 급여 권한자만 가능✅ 열 메타 requireMemoWhenSet — 값이 있는데 셀 메모가 없으면 반영을 막는다(작성자·시각이 메모에 함께 남는다). 권한은 이미 충족 — 시트 셀 쓰기가 전부 payroll:manage 를 요구한다(sheet-cell.router.ts), 그래서 새 검사를 얹지 않았다
④소득세 표가 없을 때의 거동✅ 해소requireValueOnCommit (T5)
⑤잠정/확정 표기를 Phase 1에 넣나기획서 제안대로 — 처리는 Phase 3, 표기 자리는 Phase 1에 확보. 원장과 명세서 양쪽✅ DeductionRecord.settlementState (마이그 1892) + 열 메타 settlementState + 명세서 안내. 값이 실제로 잠정일 때만 표시한다 — 임의공제만 있는 달에는 사실이 아니다
⑥두루누리 지원금의 명세서 표시표시하기로 결정. 단 이번 PR에서는 제외❌ 미구현. 표시할 값이 코드 어디에도 없다 — 전수 검색 결과 언급이 이 문서 한 곳뿐이고, 계산에 들어가지 않으며 저장되는 값도 없다. 고지서에서만 차감되므로 자동 계산이 불가능하다. 선결: 그 숫자가 어디서 오는가(관리자 월별 입력 / 고지서 import)를 정해야 화면을 만들 수 있다

리뷰어가 알아야 할 것 ​

  • ①은 이미 출하된 동작을 뒤집는다. PR #581 의 앞선 커밋은 판정 불가를 차단했는데, 기획 확정은 "통과 + 확인"이다. 같은 PR 안에서 뒤집었으므로 develop 에는 차단 동작이 간 적이 없다.
  • ②는 기획 초안을 채택하지 않은 결정이다. 초안은 개인정보 쟁점을 들어 직원 필드 승격을 제안했으나, 접근 권한이 실질적으로 같다는 판단으로 시트 열을 유지한다.
  • ③의 권한 요구는 새로 만들지 않았다. 이미 같은 권한이 걸려 있어, 얹으면 같은 규칙이 두 곳에 생긴다.
  • ⑥은 결정은 났으나 구현이 빠졌다. 데이터 출처가 정해지지 않아 지금 만들 수 있는 것은 빈 칸뿐이다.

후속: 서비스 사양서(docs/spec/) 동기화 ​

이 변경은 PM 소유 사양서가 기술하는 화면 동작을 바꾼다. 사양서는 "라이브에서 확인된 것만 기술"이 원칙이고(docs/spec/GUIDE.md), 갱신은 docs/spec-sync-* 전용 PR 로 따로 올라간다(#575·#580 선례). 그래서 이 PR 은 사양서를 건드리지 않는다 — 머지 후 동기화 대상으로 남긴다.

사양서무엇이 달라졌나
HBHR201_전체 확정 (commit)TBU 두 개가 해소된다. ① "확정 직전 검증(commit preflight) … 구현 여부 = TBU" → 차단(commitBlockers) + 확인(commitWarnings)으로 구현. ② "확정 파이프라인 실패 피드백 UI (라이브 미검증)" → 3부 구조(요약/대상/조치) 다이얼로그 + 대상 직원 이름·사번. 스킵 규칙 표에 판정 불가 확인과 사유 없는 수동 판정 차단 행이 추가돼야 한다
HBEA530_급여명세서 모달「공제 내역」 위에 잠정 안내가 새로 뜬다(잠정 레코드가 있을 때만). 현재 사양표에는 "6. 공제 내역 · 공제 항목별 금액" 한 줄뿐이다
HBHR100_시트 워크스페이스「수동 판정」 열이 사유(셀 메모) 없이는 반영을 막는다 — 셀 메모가 선택이 아니라 필수가 되는 첫 사례

POL_급여정본경계("검증/AI 는 경고·차단까지만, record 생성은 사람의 확인으로만 실행")와는 어긋나지 않는다 — 오히려 그 정책이 P1 로 정의해 둔 것을 구현한 것이다. ​

8. 참고 출처 ​