식대 검증 자동화
복리후생·지출 규정의 지원 요건을 조문 단위로 코드화하고 식대 사용 내역과 근태기록을 자동 대조. 결과가 실제 급여 차감으로 집행되며, 당사자에게 설명 가능한 차감사유 문장까지 생성.
매달 사람이 하루~이틀에 걸쳐 눈으로 대조하던 식대 부당사용 검증을, 사내 규정을 조문 단위로 코드화한 검증 엔진 + 대화형 실행 커맨드로 대체했다. 2026년 3월부터 5개월 연속 운영 중.
1. 한눈에
| 역할 | 규정 해석 · 검증 규칙 설계 · 구현 · 매월 운영 (1인) |
| 기간 | 2026-03 ~ 운영 중 (5개월) |
| 상태 | 매월 정기 실행 · 결과가 실제 급여 차감으로 집행됨 |
| 대상 | 임직원 전원 · 월 3종(중식·석식·주말/휴일) 사용 내역 |
| 스택 | Python 3 · pandas · openpyxl · Claude Code 슬래시 커맨드 |
| 규모 | 검증 엔진 876줄 · 실행 커맨드 1개 · 출력 리포트 8시트 |
| 근거 | 복리후생 운영 규정 · 지출 규정 · 식권대장 이용가이드 |
2. 배경 — 무엇이 문제였나
식대는 선지급, 후검증 구조다. 매달 초 전 직원에게 중식 포인트를 일괄 충전하고, 실제로 그 돈을 쓸 자격이 있었는지는 나중에 확인한다.
확인해야 하는 것은 세 가지였다.
- 중식 — 연차·출장·외근·원격근무한 날은 지원 대상이 아니다. 그런 날이 며칠이었는지 세고, 남은 잔액이 그만큼 남아 있는지 본다.
- 석식 — 22시 이후까지 근무한 날만 쓸 수 있다.
- 주말/휴일 — 4시간(또는 8시간) 이상 근무한 날만 쓸 수 있다.
문제는 이 판정이 두 시스템의 데이터를 사람이 눈으로 맞춰야 나온다는 것이었다. 식권대장에서 뽑은 사용 내역과 근태 시스템에서 뽑은 근무 기록을, 사람 단위로 날짜 단위로 대조한다. 임직원 전원 × 근무일 × 3종이면 월 수천 건의 판정이고, 하루 종일 또는 이틀이 걸렸다.
그리고 이 작업은 틀려도 조용했다. 한 건을 놓치면 그냥 안 걸릴 뿐이고, 잘못 잡으면 당사자가 이의를 제기해야 드러났다.
3. 무엇을 만들었나
검증 엔진(Python)과, 그것을 매달 안전하게 돌리기 위한 대화형 커맨드 한 개.
엔진 — verify_meal_allowance.py
식대 내역과 근태기록 엑셀 두 장을 받아, 규정 조문을 그대로 옮긴 규칙으로 판정하고 8개 시트짜리 리포트를 낸다.
| 시트 | 담는 것 |
|---|---|
| 최종요약_차감대상 | 인당 중식·석식·주말 차감액 + 차감사유 문장 |
| 중식검증결과 | 충전유형 · 제외일수 · 최소잔여액 · 실제잔액 · 차감액 |
| 중식제외일상세 | 어느 날 왜 제외됐는지 전건 |
| 예외처리내역 | 차감하지 않은 판단의 근거 |
| 점심미포함반차(참고) | 경계에 걸린 건 |
| 석식 / 주말휴일 검증결과 | 건별 판정 |
| 별도확인대상 | 기계가 판정할 수 없어 사람에게 넘기는 것 |
커맨드 — /verify-meal [월]
엔진은 매달 달라지는 예외를 인자로 받아야 한다(원격근무 예외자, 시프트 근무자, 수동 정상처리 건). 그 인자를 매달 기억해서 타이핑하는 건 그 자체로 실패 지점이다. 그래서 커맨드가 먼저 물어보고, 파일을 찾고, 실행하고, 결과를 요약한 뒤, 별도확인대상이 나오면 다시 물어서 재실행한다.
자동화의 대상은 계산만이 아니라 매달 반복되는 실행 절차 전체였다.
4. 아키텍처
[식권대장] [근태 시스템]
사용자별 식대 내역 일별 근태기록 리포트
│ xlsx │ xlsx
└──────────────┬───────────────┘
▼
┌───────────────────┐
│ 정규화 계층 │ 사번 표기 통일 · 퇴사자 판정
│ load_data() │ 결측 사번 보완 · 동명이인 식별
└─────────┬─────────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
verify_lunch verify_dinner verify_weekend
(제외일 산정 (22시 기준) (4h/8h 기준)
+ 잔액 대조)
│ │ │
└─────────────┼─────────────┘
▼
┌───────────────────┐
│ 집계 · 사유 생성 │ 차감사유 문장 자동 작성
│ + 가드레일 경보 │ 풀충전 비율 이상 감지
└─────────┬─────────┘
▼
8시트 Excel 리포트
│
┌────────────┴────────────┐
▼ ▼
차감 집행 대상 별도확인대상 → 사람
- 판정 로직 3종은 서로 독립이지만 정규화 계층을 공유한다. 사번 표기가 어긋나면 세 판정이 동시에 조용히 무너지기 때문에, 정규화를 진입점 한 곳에 몰았다.
- 마지막 갈래가 이 설계의 핵심이다. 기계가 판정한 것과 판정할 수 없어서 사람에게 넘긴 것을 분리해서 출력한다.
5. 실제 난이도는 계산이 아니었다
규정을 코드로 옮기는 것 자체는 어렵지 않았다. 5개월을 버티게 만든 것은 다음 네 가지였다.
① 입력 데이터가 매달 모양을 바꾼다
근태 리포트의 사번 표기가 달마다 달랐다. 2026-05엔 소수점이 붙은 1.0, 06엔 정수 1, 07엔 선행 0을 채운 문자열 '001'. 식대 내역은 계속 소수점 형식이었다.
원본끼리 비교하면 '001' != 1이 되어 사번 조인이 전부 실패한다. 그런데 조인이 실패해도 에러는 나지 않는다. 이름 매핑, 중도입사자 편입, 재량근로 판정이 한꺼번에 무력화된 채 그럴듯한 리포트가 나온다.
→ 숫자 사번은 소수점·선행 0을 제거하고, 영문 접두 사번은 원형을 유지하는 표준화 함수를 조인 이전 단일 지점에 배치.
② 판정 실패가 “0원”으로 위장한다
가장 위험한 실패는 에러가 아니라 차감액 0원이었다.
- 근태 시스템에 기록이 아예 없는 인원 → 제외일을 셀 수 없음 → 차감 0 → 정상처럼 보임
- 충전 사유 형식이 바뀌어 “풀충전”으로 인식되지 않음 → 검증 대상에서 빠짐 → 차감 0
실제로 2026-06에 두 번째가 발생했다. 충전 사유 표기가 근무일 N일 계열에서 21일 - 2일 = 19일 x 12,000원 형식으로 바뀌었고, 판별 정규식이 이를 놓쳐 다수가 부분충전으로 오분류됐다.
→ 두 겹으로 막았다. (a) 근태기록 없는 인원은 차감 0으로 조용히 통과시키지 않고 별도확인대상으로 승격. (b) 풀충전 비율이 50% 아래로 떨어지면 “충전 사유 형식이 또 바뀌었을 가능성”을 명시적으로 경고. 조용한 0을 시끄럽게 만드는 장치다.
③ 정책이 도중에 바뀐다
2026-07에 석식·주말 식대가 개인카드 결제 후 청구로 전환됐다. 같은 달에 간주근로제가 재량근로시간제로 이름이 바뀌었고, 전환기에는 두 명칭이 데이터에 공존했다.
→ 정책 변경을 코드 수정이 아니라 플래그로 흡수했다(--card-only-dinner-weekend). 과거 월을 재검증할 때 그때의 규정으로 돌려야 하기 때문이다. 근무유형 판정은 두 명칭을 함께 매칭하도록 열어 뒀다.
④ 경계는 직원에게 유리한 쪽으로 정한다
오전에 끝나는 반차의 경우, 그날 실제 출근 시각을 함께 본다. 그런데 근태 시스템이 휴가 시간과 겹치는 근무를 허용하지 않아서, 11:59에 찍은 출근이 휴가 종료 시각인 12:00 정각으로 당겨져 기록된다.
즉 12:00 표기는 “11:59에 찍었다”와 “진짜 12:00에 찍었다”를 구분할 수 없다.
→ 12:00 정각은 지급 처리(직원에게 유리), 12:01 이후는 차감. 스냅은 시각을 앞으로 당기기만 하므로 12:01 이후 표기는 지각이 확정이다. 판정 불가능한 구간을 인정하고, 그 구간에서 누구 편을 들지를 명시적으로 정했다.
6. 기술적 의사결정
① 차감액이 아니라 “차감사유 문장”을 출력물로 삼았다
연차(종일) 2일, 연차(반차) | 3일×12,000-0=36,000 형식의 문장을 자동 생성한다. 차감은 당사자에게 설명할 수 있어야 집행 가능하다. 숫자만 있는 리포트는 결국 사람이 다시 근거를 찾아야 하므로, 자동화의 효과가 절반에서 끊긴다.
② 예외 처리 내역을 “차감하지 않은 이유”로 따로 남겼다 검증의 산출물은 보통 걸린 것만 담는다. 여기서는 외근이지만 점심시간에 사무실 근무 기록이 있어 지급한 건, 시프트 근무자라 부분휴가를 지급한 건 등 차감하지 않기로 한 판단을 별도 시트로 남긴다. 나중에 “왜 이 사람은 안 걸렸나”를 물었을 때 답할 수 있어야 한다.
③ 이중 차감을 막기 위해 전월 집행분을 복원한다
전월 검증 결과가 당월 데이터에 식대 차감으로 반영되어 잔액을 낮춘다. 그 상태로 다시 검증하면 같은 사유로 두 번 차감된다. 전월 차감분을 되돌린 잔액을 기준으로 판정하되, 당월 내 상쇄성 차감(차감 ↔ 재지급)은 순효과가 0이므로 복원 대상에서 제외했다.
④ 월말 소멸·퇴사 정산 행을 잔액 기준에서 뺐다 월말이면 잔액이 0으로 소멸하고, 퇴사자는 초기화 행이 들어온다. 마지막 행을 그대로 잔액으로 쓰면 전원이 0원이 되어 검증이 무의미해진다. 소멸·초기화 직전 잔액을 기준으로 삼는다.
⑤ 퇴사 예정자를 성급히 제외하지 않는다 근태 리포트에는 다음 달 퇴사 예정자의 퇴사일이 미리 기입되어 온다. 이를 그대로 제외하면 정상 재직한 달의 사용 내역이 통째로 미검증으로 빠진다. 퇴사일이 검증 월 말일 이후면 검증 대상으로 유지한다.
7. 숫자로 본 결과
| 항목 | 수치 |
|---|---|
| 연속 운영 | 5개월 (2026-03 ~ 07) |
| 월 소요 시간 | 하루~이틀 → 수 분 (예외 확인 포함 30분 내외) |
| 월평균 차감 대상 | 23명 |
| 리포트 산출물 | 월 8시트 · 판정 근거 전건 포함 |
| 검증 규칙 | 3종(중식·석식·주말) · 예외 규칙 8종 이상 |
| 판정 불가 건 처리 | 별도확인대상으로 분리 — 조용한 통과 0건 |
월별 차감 인원·금액 실적은 사내 수치이므로 이 문서에는 요약만 담았다. 외부 제출 시에는 이 항목을 빼거나 회사 확인을 받는 것을 권장한다.