Skip to content

급여대장 데이터 프로비넌스

원천 엔티티는 관측값만 보관합니다. OvertimeRecord, FixedIncentive, EducationSubsidy, LeaveRequest, ResignationRecord, YearEndSettlement에는 계산된 금액 필드가 없습니다. 모든 금액은 AllowanceRecord / DeductionRecord에서만 관리되며, 이 레코드들은 시트의 "✓ 커밋" 작업 결과로 생성되어 시트 계보(sourceSheetId / sourceSheetRowId / sourceSheetColumnKey, NOT NULL)를 가집니다. 보험/세금/수당 금액은 컬럼별 계산기가 산출하며, 그 파라미터는 시트 컬럼 메타데이터 + 조직 파라미터 저장소에 보관됩니다. 참고: Payroll Schema Refactor

급여대장(PayrollRecord) 생성에 관여하는 모든 테이블의 컬럼 정의, 데이터 흐름, AllowanceRecord-as-atom 아키텍처를 기록합니다.

아키텍처 원칙

AllowanceRecord가 모든 지급의 원자(atom)입니다. 기본급, 초과근무수당, 각종 수당, 인센티브 등 모든 지급 항목은 개별 AllowanceRecord로 생성됩니다. 보험료, 세금, 정산 등 모든 공제 항목은 개별 DeductionRecord로 생성됩니다. PayrollRecord는 이 원자 레코드들을 FK로 연결하는 헤더이며, 캐시된 합계를 저장합니다.

추가전용(Append-Only) 메타 테이블

모든 메타 테이블(EmploymentContract, OvertimeRecord, FixedIncentive, YearEndSettlement, EducationSubsidy, ResignationRecord)은 추가전용입니다. 내용을 직접 수정하지 않고, 새 버전 행을 삽입하며 이전 행에 supersededAt를 설정합니다. 이를 통해 전체 변경 이력이 보존됩니다.

컬럼타입용도
chainIduuid동일 논리 엔티티의 모든 버전을 그룹화
versioninteger체인 내 단조 증가 (1부터 시작)
supersededAttimestamp, nullableNULL = 현재 버전; 새 버전 삽입 시 설정됨
  • 내용 변경 (금액, 날짜, 요율): 새 버전 생성 (version + 1, 동일 chainId)
  • 상태 변경 (pending → approved → paid): 동일 행에 직접 메타데이터 업데이트
  • 조회 패턴: 현재 버전만 조회 시 WHERE supersededAt IS NULL
  • 이력 조회: WHERE chainId = :chainId ORDER BY version DESC로 전체 감사 추적

프로비넌스 추적

AllowanceRecord와 DeductionRecord는 어떤 메타 테이블 행이 자신을 생성했는지 추적합니다:

컬럼타입용도
sourceTypeenum (PayrollSourceType)어떤 메타 테이블이 이 레코드를 생성했는지
sourceIduuid, nullable특정 메타 테이블 행 ID (집계된 원천의 경우 null)

PayrollSourceType 값 (packages/core/src/enums/payroll.enum.ts): employment_contract, overtime_record, fixed_incentive, education_subsidy, year_end_settlement, resignation_record, payroll_rate_config, tax_bracket_config, manual

전체 데이터 흐름도

핵심 불변식 (2026-08-13 검증): 금액 원자 AllowanceRecord/DeductionRecord오직 시트 커밋(commitSheet)에서만 생성된다. generateForPeriod는 원자를 만들지도 지우지도 않고, PayrollRecord 헤더 스냅샷을 만들어 기존 원자를 payrollRecordId로 링크만 한다. (이 문서의 이전 버전은 generateForPeriodisAutoGenerated로 필터해 원자를 생성·삭제한다고 기술했으나 이는 틀렸고 위험하다 — 아래 흐름 참조.)


흐름: 시트 커밋 vs 스냅샷 생성

금액 원자 생성 = 시트 커밋 (commitSheet)

  • AllowanceRecord/DeductionRecord오직 시트 "✓ 커밋"에서 생성된다. compute.ts가 원천 관측을 승인 상태 게이트(예: 초과근무 status IN ('approved','report_pending','report_approved','report_rejected'))로 SQL 집계하고, commitSheet가 그 결과를 AR/DR로 기록한다(isAutoGenerated = true).
  • 재커밋: 해당 sourceSheetId의 기존 AR/DR을 deletedAt으로 soft-delete한 뒤 재삽입한다(전부 isAutoGenerated = true).
  • 정산 라이터(연말정산·퇴직정산)만 isAutoGenerated = false로 AR/DR을 쓴다 — 이는 "수동 입력"이 아니라 "정산 계열 출력"이라는 뜻이며, 자체 cancel-cleanup 게이트로 쓰인다.

generateForPeriod (PayrollRecord 스냅샷 전용 — 원자 삭제 안 함)

  1. 해당 기간이 이미 CONFIRMED면 차단(PRECONDITION_FAILED).
  2. 기간의 낡은 PayrollRecord만 삭제. AR/DR은 삭제하지 않음payrollRecordId = NULL로 unlink만.
  3. PayrollRecord 헤더(캐시 요약 합계) 재생성.
  4. 기간의 모든 AR/DR을 payrollRecordId로 재링크.

⚠️ 하지 말 것: isAutoGenerated = true 필터로 AR/DR을 삭제·재생성하는 로직을 generateForPeriod에 되살리지 말 것. 그 필터는 의도적으로 제거됐다 — AR/DR은 시트 커밋 산출물이라 여기서 지우면 시트가 커밋한 급여 데이터가 소실된다. 근거: packages/api/src/routers/payroll.router.tsgenerateForPeriod 인라인 주석 + .claude/rules/payroll/korean-payroll-compliance.md.


⚠️ 구현 모호점 · 하드닝 후보 (2026-08-13 감사)

  1. 초과근무 집계 이중 경로(SSOT 미정) — 실 급여 경로는 sheet-rows/compute.ts(SQL, 상태 게이트)이나, 별도 OvertimeRecordCalculator(packages/api/src/calculators/overtime-record.calculator.ts)는 status 필터 없이 supersededAt IS NULL만으로 합산한다. 현재는 Module Manager 상태 조회에서만 생성되어 급여를 만들지 않지만, 급여 경로로 재배선하면 대기·취소 초과근무가 새어든다 → 재배선 시 status 게이트 필수.
  2. compute.tsreport_rejected(보고서반려)를 급여에 포함 — 주석("pending/rejected/cancelled 제외")과 IN 목록이 불일치. 정책 문서("보고서 승인 시점 반영")와 어긋남 → 반영 기준 확정 필요.
  3. isAutoGenerated = false = "정산 라이터 출력"(수동 아님) — cancel-cleanup 분기 수정 시 오해 주의.
  4. 휴일 마스터 ↔ 휴일수당 계산 미연결OrganizationHoliday는 존재(연차·출근율 소비)하나 휴일수당 계산 경로는 미소비 — 수동 HOLIDAY/Shift.treatAsHoliday로만 발생. 연결 여부 판단 필요.
  5. NIGHT/HOLIDAY_NIGHT 유효 2.0x 이중 버킷 — 버킷 누락 시 조용한 과소지급(과거 2회 재발). 모듈 레벨 회귀 가드는 신설됨(packages/api/src/calculators/payroll-correctness.test.ts) — 계산기 레벨 카테고리→버킷 매핑과 국민연금 상·하한(cap/floor)은 여전히 미커버.

1. Employee (직원)

테이블: employees

컬럼타입급여대장 사용
iduuid (PK)AllowanceRecord.employeeId FK
employeeNumberstring, uniqueUI 표시, eCount name 조회
firstName / lastNamestringeCount export: ${lastName}${firstName}
statusenum급여 생성 대상 필터 (ACTIVE만)
organizationIduuid (FK)멀티테넌시 필터

2. EmploymentContract (근로계약)

테이블: employment_contracts메타 테이블. AllowanceRecord 생성의 입력 데이터.

컬럼타입급여대장 사용
baseSalarydecimal(15,2)→ AllowanceRecord (itemCode 01 월급여)
allowancesjsonb→ AllowanceRecords (식대→19, 교통비→14, 주거비→14)
salaryGradeIduuid (FK)SalaryGrade 참조
isActiveboolean활성 계약만 사용

3. OvertimeRecord (초과근무 기록)

테이블: overtime_records원시 관측 테이블. 금액은 OvertimeRecordCalculator가 계산(overtimeHours × Contract.baseSalary/209 × 배수 — 배수는 시트 컬럼/조직 파라미터 저장소에서 가져옴)해 시트 커밋 시 AllowanceRecord에 기록합니다.

컬럼타입급여대장 사용
employeeIduuid (FK)집계 키
workDatedate기간 필터
overtimeHoursdecimal(7,2)→ 계산기 입력
mealBreakHoursdecimal(7,2)식사시간 차감
categoryenum카테고리 → AllowanceRecord itemCode 결정
statusenum시트 집계 게이트: approved / report_pending / report_approved / report_rejected (sheet-rows/compute.ts)
hourlyRatedecimal(15,2)(제거예정) — 계산 시 재계산
multiplierdecimal(5,2)(제거된 컬럼) — 값은 이제 시트 컬럼/조직 파라미터 저장소에 보관
calculatedPaydecimal(15,2)(제거예정)AllowanceRecord.amount에 보관

카테고리 → AllowanceRecord ItemCode (레거시 — 마이그레이션 1816 이전)

⚠️ 레거시 매핑. 마이그레이션 1816 이후 AllowanceRecord에는 item-code FK가 없다 — 레코드는 targetSheetSlug/targetColumnKey로 시트 컬럼을 가리키며, eCount A/D 코드 분류는 export 시점에 수행된다. 아래 표는 폐기된 고정 매핑 기록이다(HOLIDAY_NIGHT 도입 전 표 — HOLIDAY_NIGHT은 휴일 1.5 + 야간 0.5 버킷 합산).

categoryAllowanceRecord itemCode항목명
WEEKDAY10연장근무수당1
NIGHT12연장근무수당2
SATURDAY13야간근무수당1
HOLIDAY09휴일근무수당

4. AllowanceRecord (수당 기록 — 원자)

테이블: allowance_records모든 지급의 원자(atom). 단일 진실 공급원.

컬럼타입사용
iduuid (PK)기본키
employeeIduuid (FK)직원 참조
year / monthnumber기간
amountdecimal(15,2)지급 금액
typeenumAllowanceType (자동생성 시 OTHER)
paymentMethodenumSALARY: 급여 포함
reasonstring사람이 읽을 수 있는 설명
statusenumAPPROVED / PAID
itemCodeIduuid (FK)AllowanceItemCode FK (eCount 코드)
payrollRecordIduuid (FK)PayrollRecord FK (generate 시 연결)
isAutoGeneratedbooleantrue = 시트 커밋 출력(commitSheet); false = 정산 라이터(연말/퇴직), "수동"이 아님
sourceTypeenumPayrollSourceType — 어떤 메타 테이블이 생성했는지
sourceIduuid, nullable특정 메타 행 ID (집계 원천의 경우 null)
organizationIduuid (FK)멀티테넌시

시트 커밋 AllowanceRecords (commitSheet)

데이터 원천itemCodereason금액sourceTypesourceId
Contract.baseSalary01월급여baseSalaryemployment_contractcontract.id
OvertimeRecord (WEEKDAY)10연장근무수당1Σ calculatedPayovertime_recordnull
OvertimeRecord (NIGHT)12연장근무수당2Σ calculatedPayovertime_recordnull
OvertimeRecord (SATURDAY)13야간근무수당1Σ calculatedPayovertime_recordnull
OvertimeRecord (HOLIDAY)09휴일근무수당Σ calculatedPayovertime_recordnull
Contract.allowances.meal19식대식대 금액employment_contractcontract.id
Contract.allowances.transportation14교통비교통비 금액employment_contractcontract.id
Contract.allowances.housing14주거비주거비 금액employment_contractcontract.id
FixedIncentive23간호간병평가인센티브Σ totalAmountfixed_incentivenull

5. DeductionRecord (공제 기록 — 원자)

테이블: deduction_records모든 공제의 원자(atom). 단일 진실 공급원.

컬럼타입사용
iduuid (PK)기본키
employeeIduuid (FK)직원 참조
year / monthnumber기간
amountdecimal(15,2)공제 금액
reasonstring사람이 읽을 수 있는 설명
statusenumAPPROVED / APPLIED
itemCodeIduuid (FK)DeductionItemCode FK (eCount 코드)
payrollRecordIduuid (FK)PayrollRecord FK (generate 시 연결)
isAutoGeneratedbooleantrue = 시트 커밋 출력(commitSheet); false = 정산 라이터(연말/퇴직), "수동"이 아님
sourceTypeenumPayrollSourceType — 어떤 메타 테이블이 생성했는지
sourceIduuid, nullable특정 메타 행 ID (집계 원천의 경우 null)
organizationIduuid (FK)멀티테넌시

시트 커밋 DeductionRecords (commitSheet)

보험 요율은 InsuranceCalculator에 바인딩된 조직 파라미터 저장소/시트 컬럼에서 가져오며, 행은 시트 커밋 시 기록됩니다.

데이터 원천itemCodereason계산 공식
InsuranceCalculator03국민연금round(clamp(base, floor, cap) × rate)
InsuranceCalculator04건강보험round(baseSalary × rate)
InsuranceCalculator05고용보험round(baseSalary × rate)
InsuranceCalculator20장기요양보험round(baseSalary × rate)
TaxBracketConfig01소득세round((과세소득 × 세율 - 누진공제) / 12)
TaxBracketConfig02주민세round(소득세 × 0.10)

6. FixedIncentive (고정 인센티브)

테이블: fixed_incentives메타 테이블. AllowanceRecord 생성의 입력 데이터.

컬럼타입급여대장 사용
employeeIduuid (FK)집계 키
year / monthnumber기간 필터
unitPricedecimal(15,2)단가
unitCountint단위 수량
totalAmountdecimal(15,2)→ AllowanceRecord (itemCode 23)
categoryenum인센티브 카테고리 (재활, 간호 등)

7. YearEndSettlement (연말정산)

테이블: year_end_settlements원시 관측/라이프사이클 테이블. 조정 금액은 시트 커밋 시 DeductionRecord 행으로 생성됩니다. 분할 납부 계획은 시트 컬럼/조직 파라미터 저장소에 설정합니다.

컬럼타입급여대장 사용
employeeIduuid (FK)직원 참조
settlementYearnumber정산 연도
applicationDatedate, nullable조정 적용 일자
applicationMonthnumber, nullable적용 급여 월
statusenumPENDING → CALCULATED → APPROVED → APPLIED → COMPLETED
adjustmentAmountdecimal(15,2)(제거예정)DeductionRecord.amount (itemCode 10)로 이동
installmentMonthsnumber(제거된 컬럼) — 분할 계획은 이제 시트 컬럼/조직 파라미터 저장소에 보관
installmentAmountdecimal(15,2)(제거예정)adjustmentAmount / installmentMonths로 파생

8. EducationSubsidy (교육보조금)

테이블: education_subsidies원시 관측/라이프사이클 테이블. 보조금 금액은 EducationSubsidyCalculator를 통해 시트 커밋 시 AllowanceRecord로 생성됩니다. 단가(교육과정 유형별)는 시트 컬럼/조직 파라미터 저장소에 설정합니다.

컬럼타입급여대장 사용
employeeIduuid (FK)직원 참조
courseNamestring교육과정 식별
paymentDatedate지급 시점
receiptVerifiedboolean영수증 확인 상태
statusenumPENDING → APPROVED → PAID → CANCELLED
courseAmountdecimal(15,2)(제거예정)AllowanceRecord.amount로 이동

9. ResignationRecord (퇴직정산)

테이블: resignation_records원시 관측/라이프사이클 테이블. 정산 금액은 ResignationRecordCalculator를 통해 개별 AllowanceRecord / DeductionRecord 행으로 분해됩니다.

컬럼타입급여대장 사용
employeeIduuid (FK)직원 참조
resignationDatedate퇴직일
reasonstring, nullable퇴직 사유
statusenumPENDING → PROCESSING → SETTLED → COMPLETED
settlementAmountdecimal(15,2)(제거예정) — 개별 원장 행으로 분해
healthInsuranceRecondecimal(15,2)(제거예정)DeductionRecord itemCode 12
yearEndRecondecimal(15,2)(제거예정)DeductionRecord itemCode 10
annualLeavePayoutdecimal(15,2)(제거예정)AllowanceRecord itemCode 17
unusedLeaveDaysdecimal(5,2)(제거예정) — 연차 발생 − 사용에서 파생
totalSettlementdecimal(15,2)(제거예정) — 원장 행 합계

10. 4대보험 요율

4대보험 요율(국민연금/건강보험/고용보험/장기요양보험 근로자 요율, 국민연금 상·하한)은 InsuranceCalculator조직 파라미터 저장소payroll_rate_config codeModule 키에서 읽으며, 해당 공제 컬럼을 보험 계산기에 바인딩하는 시트 컬럼 메타데이터와 함께 사용됩니다. 계산된 금액은 시트 커밋("✓ 커밋") 시에만 DeductionRecord로 기록되며 시트 계보 3종을 가집니다. 참고: Payroll Schema Refactor.


11. TaxBracketConfig (소득세 구간)

테이블: tax_bracket_configs

컬럼타입급여대장 사용
minIncome / maxIncomedecimal(15,2)구간 판별
ratedecimal(8,4)→ DeductionRecord 금액 계산
deductiondecimal(15,2)→ 누진공제
effectiveYearnumber연도 필터

12–16. 참조 테이블

SalaryGrade (salary_grades)

호봉별 급여 정의. EmploymentContract.salaryGradeId를 통한 간접 참조.

AllowanceItemCode (allowance_item_codes)

eCount 수당 항목코드 마스터. AllowanceRecord.itemCodeId FK.

DeductionItemCode (deduction_item_codes)

eCount 공제 항목코드 마스터. DeductionRecord.itemCodeId FK.


17. PayrollRecord (급여대장 헤더)

테이블: payroll_records — AllowanceRecord/DeductionRecord를 연결하는 헤더 레코드. 월별/직원별 1레코드.

관계

관계타입설명
allowanceRecordsOneToMany → AllowanceRecord이 급여의 모든 지급
deductionRecordsOneToMany → DeductionRecord이 급여의 모든 공제
detailsOneToMany → PayrollDetail항목별 상세 (하위호환)

캐시된 합계 필드

생성 시 연결된 AllowanceRecords/DeductionRecords에서 계산된 값:

컬럼타입계산 원천
baseSalarydecimal(15,2)AllowanceRecord (itemCode 01)
overtimePaydecimal(15,2)AllowanceRecord (itemCode 10)
nightDifferentialdecimal(15,2)AllowanceRecord (itemCode 12)
weekendDifferentialdecimal(15,2)AllowanceRecord (itemCode 13)
holidayDifferentialdecimal(15,2)AllowanceRecord (itemCode 09)
allowancesjsonbAllowanceRecords (식대 등)
totalEarningsdecimal(15,2)Σ 전체 AllowanceRecords
nationalPensiondecimal(15,2)DeductionRecord (itemCode 03)
healthInsurancedecimal(15,2)DeductionRecord (itemCode 04)
employmentInsurancedecimal(15,2)DeductionRecord (itemCode 05)
longTermCareInsurancedecimal(15,2)DeductionRecord (itemCode 20)
incomeTaxdecimal(15,2)DeductionRecord (itemCode 01)
localIncomeTaxdecimal(15,2)DeductionRecord (itemCode 02)
totalDeductionsdecimal(15,2)Σ 전체 DeductionRecords
netPaydecimal(15,2)totalEarnings - totalDeductions

상태 및 승인

컬럼타입사용처
statusenumDRAFT→APPROVED→CONFIRMED→PAID
paymentDatedate생성 시 25일로 설정
approvedBy / approvedAtstring / timestamp승인 워크플로우
confirmedBy / confirmedAtstring / timestamp확정 워크플로우
paidAttimestamp지급 처리

18. PayrollDetail (급여 상세)

테이블: payroll_details — PayrollRecord의 항목별 상세 내역 (하위호환).

컬럼타입설명
payrollRecordIduuid (FK)급여 레코드
itemCodestring항목 코드 (BASE_SALARY, OVERTIME 등)
itemNamestring항목명 (기본급, 초과근무수당 등)
itemTypeenumEARNING / DEDUCTION / ADJUSTMENT
categoryenumBASE_SALARY / OVERTIME / ALLOWANCE / TAX / INSURANCE 등
amountdecimal(15,2)금액

19. eCount Export

eCount export는 AllowanceRecordDeductionRecord 테이블에서 직접 읽습니다. PayrollRecord 필드가 아닌, (employeeId, itemCodeId) 별 집계를 사용합니다.

A-Codes (수당) — allowance_item_codes

코드항목명원천
A01월급여AllowanceRecord (itemCode 01)
A02초과수당AllowanceRecord (itemCode 02)
A03직책수당AllowanceRecord (itemCode 03)
A04면허자격수당AllowanceRecord (itemCode 04)
A05위험수당AllowanceRecord (itemCode 05)
A06장기근속수당AllowanceRecord (itemCode 06)
A07나이트수당1 - 간호사AllowanceRecord (itemCode 07)
A08나이트수당2 - 요양보호사AllowanceRecord (itemCode 08)
A09휴일근무수당AllowanceRecord (itemCode 09)
A10연장근무수당1AllowanceRecord (itemCode 10)
A11특별수당AllowanceRecord (itemCode 11)
A12연장근무수당2AllowanceRecord (itemCode 12)
A13야간근무수당1AllowanceRecord (itemCode 13)
A14기타AllowanceRecord (itemCode 14)
A16호봉수당AllowanceRecord (itemCode 16)
A17연차수당 (퇴사자)AllowanceRecord (itemCode 17)
A19식대AllowanceRecord (itemCode 19)
A20상여AllowanceRecord (itemCode 20)
A21특별수당1AllowanceRecord (itemCode 21)
A23간호간병평가인센티브AllowanceRecord (itemCode 23)
A24내일채움공제AllowanceRecord (itemCode 24)

D-Codes (공제) — deduction_item_codes

코드항목명원천
D01소득세DeductionRecord (itemCode 01)
D02주민세DeductionRecord (itemCode 02)
D03국민연금DeductionRecord (itemCode 03)
D04건강보험DeductionRecord (itemCode 04)
D05고용보험DeductionRecord (itemCode 05)
D06퇴직정산DeductionRecord (itemCode 06)
D08기타공제DeductionRecord (itemCode 08)
D10연말정산DeductionRecord (itemCode 10)
D11건강, 장기연말정산DeductionRecord (itemCode 11)
D12건강, 장기퇴직정산DeductionRecord (itemCode 12)
D13고용보험정산DeductionRecord (itemCode 13)
D20장기요양보험DeductionRecord (itemCode 20)

계산 공식 요약

지급 (AllowanceRecords)
───────────────────────────────────────
계약에서 자동생성:
  월급여 (01)       = Contract.baseSalary
  식대 (19)         = Contract.allowances.meal
  교통비 (14)       = Contract.allowances.transportation
  주거비 (14)       = Contract.allowances.housing

초과근무에서 자동생성:
  연장근무수당1 (10) = Σ OvertimeRecord(WEEKDAY).calculatedPay
  연장근무수당2 (12) = Σ OvertimeRecord(NIGHT).calculatedPay
  야간근무수당1 (13) = Σ OvertimeRecord(SATURDAY).calculatedPay
  휴일근무수당 (09)  = Σ OvertimeRecord(HOLIDAY).calculatedPay

인센티브에서 자동생성:
  인센티브 (23)      = Σ FixedIncentive.totalAmount

수동 AllowanceRecords:
  (각종 itemCode, 승인 워크플로우를 통해 생성)

totalEarnings = Σ 전체 AllowanceRecords (자동 + 수동)

공제 (DeductionRecords)
───────────────────────────────────────
자동생성 (보험):
  국민연금 (03)     = round(clamp(baseSalary, floor, cap) × rate)
  건강보험 (04)     = round(baseSalary × rate)
  고용보험 (05)     = round(baseSalary × rate)
  장기요양보험 (20) = round(baseSalary × rate)

자동생성 (세금):
  과세소득          = totalEarnings - 보험료합계
  소득세 (01)       = round((과세소득 × 세율 - 누진공제) / 12)
  주민세 (02)       = round(소득세 × 0.10)

수동 DeductionRecords:
  (각종 itemCode, 승인 워크플로우를 통해 생성)

totalDeductions = Σ 전체 DeductionRecords (자동 + 수동)

순지급액
───────────────────────────────────────
netPay = totalEarnings - totalDeductions