E2E Test Scenario — 시스템 템플릿 참조 수식(formula) 수당 계산의 월 전환 안정성
Status: Verified · 2026-06-11 로컬 전체 실행 (org
SHEETRH, M=2026-06 → M+1=2026-07 — 전 단계 통과)
검증 결과 요약 (2026-06-11 실행)
- 참조 방식 ①②③ 전부 기대값 일치 (박치료 · 시급 14,383.56 파생): wd 215,754 → PENDING 4h 승인 후 302,055 (게이트 반영 즉시 재계산 확인) · cross-sheet posAlw 30,000 / ntMeal 8,000 · 수식→수식 체인 otPay = ROUND(302,055+14,384+8,000) = 324,439 · IF 분기 posPay 30,000/0.
- 수식 컬럼 커밋: 대상 급여대장 aggregate 컬럼 페어(
targetSheetSlug/Key) 스탬프 +inputSnapshot.formula에 수식 원문 기록 (항목코드는 그 aggregate 컬럼에 — 2026-06-19 모델) + 재커밋 idempotent(insert 0/update 4). 0원 수식 레코드는 생성됨(기존 동작 — eCount 출력 제외). - 🆕 4차 수정 1차 검증 통과 — 이월(carry-forward in-batch) · 사용자 템플릿 적용 · JSON import 세 경로 모두 수식 안
[<uuid>:colKey]가 7월 "수당 기준값" uuid 로 재작성, 6월 uuid 잔존 없음. 음성 케이스(대상 기간에 동명 시트 부재 — 8월 적용)는 원본 uuid 유지. - 7월 재계산: 행/값 미복사(0행) → 7월 입력만으로 wd 64,727 (승인 3h만 — pending 5h/1h 제외) · posAlw 35,000 (7월에 새로 입력한 값 — 재매핑된 참조가 새 달 시트를 실제로 읽음) · 신규 입사자 40,274 합류 · lineage 7월 시트.
- 격리/회귀: 6월 레코드(324,439/30,000)·수식 uuid·재열람 값 전부 불변 · 수식 셀 직접 편집 서버 400.
- UI 수식 에디터: ƒ 수식 편집 팝오버에 수식 원문 표시, 편집 중 다른 컬럼 헤더 클릭 →
[colKey]토큰 삽입(onHeaderInjectRef), 취소 시 DB 미반영 — 동작 확인. - 📌 부록 C 1번 실측 답: 재매핑 미해석(stale uuid) 시 수식은 전월 시트 값을 조용히 그대로 읽는다 (8월 음성 케이스에서 posAlw = 6월 값 30,000) — null 이 아니라 조용한 오답이므로, 참조 대상 시트를 대상 기간에 먼저 만드는 운영 순서(STEP 5)가 중요하다.
Branch context:
develop— sheet-rooted records 선행 문서: test-scenario-sheets-monthly-reuse.md (Verified 2026-06-11, orgSHEETRH) — 시트 복제 4경로·편집 정책·일반 함정은 그 문서가 소유한다. 본 문서는 그 위에 "수식이 시스템 템플릿을 참조해 수당을 계산하고, 월이 바뀌어도·이월해도 계속 동작하는가" 한 가지를 깊게 검증한다.
목적
관리자가 시스템 템플릿(연장근무 내역 전체 등)을 데이터 소스로 두고, 수식(formula) 컬럼으로 수당을 직접 설계하는 시나리오:
- M월 — 수식 컬럼이 시스템 템플릿/타 시트의 값을 참조해 수당 금액을 산출하고, 그 수식 컬럼을 커밋해 AllowanceRecord 를 만든다.
- M+1월 — 같은 구성을 "이전 월에서 가져오기"(carry-forward)로 가져왔을 때, 수식의 시트 참조가 새 달의 시트로 재매핑되어 새 달 데이터로 재계산·재커밋된다.
- 시스템 템플릿 재적용(slug idempotent)과 수식 참조가 함께 동작한다.
테스트 기간: M = 2026-06, M+1 = 2026-07. 조직은 선행 문서의 SHEETRH 재사용(권장 — 직원·계약·항목코드 이미 있음) 또는 신규 프로비저닝.
🆕 4차 수정 (2026-06-11, 본 문서 작성 중 선수정) — 본 시나리오의 1차 검증 대상
수식 에디터의 cross-sheet 참조 빌더는 [<sheetId>:colKey] 형태로 시트 uuid 를 수식 문자열 안에 박는다. 3차 수정(sheet-ref-remap.ts)은 aggregate 의 sourceSheetId 핀만 재매핑했고 수식 안의 uuid 는 손대지 않았다 → 이월/템플릿 적용/JSON import 된 시트의 수식이 전월 시트를 계속 읽는 조용한 오답 경로가 남아 있었다.
수정: 헬퍼를 remapSheetRefs 로 확장 — columnLayout 의 formula 문자열에서 [<uuid>: 토큰을 수집해 aggregate 핀과 같은 규칙(같은 이름 → 대상 기간 시트, in-batch 사본 우선)으로 재작성. carryForward · sheetTemplate.applyToPeriod · sheetView.importTemplate 세 경로 모두 통과한다. (선행 문서 3차 메모의 "formula 는 시트 간 참조가 없다"는 서술은 오류였음 — 본 문서로 정정.)
핵심 전제 — 수식이 시스템 템플릿을 참조하는 4가지 방식
| # | 방식 | 컬럼 구성 | 월 전환 안정성 |
|---|---|---|---|
| ① | computed 모듈 | kind:"computed" + computeSourceModule:"overtime_record" + computedKey(weekday_base/night_base/holiday_base/weekend_base/total_base/*_hours/*_days) — 시트가 아니라 source 테이블을 기간 스코프로 집계 (overtime-record.ts) | ✅ 설계상 안정 — host 시트의 (year,month)로 재집계. ⚠️ 승인 게이트 통과 건만 집계 (approved/report_* — 2026-06-11 결정, pending 제외) |
| ② | row-local 수식 | kind:"formula", [colKey] 로 같은 행의 identity/ref/computed/literal/타 수식 참조 | ✅ 안정 — 행 안에서 닫힘 |
| ③ | cross-sheet 수식 | 수식 안 [<sheetId>:colKey] — 3형: 1-part(같은 직원 행 조회) · 2-part(amountCol:memberCol 분배합) · 3-part(VLOOKUP: valueCol:keyCol:myKeyCol) (resolver.ts:2704) | 🆕 4차 수정으로 안정화 — 이월/적용/import 시 uuid 가 같은 이름의 대상 기간 시트로 재작성. 본 시나리오가 첫 검증 |
| ④ | aggregate 컬럼 | kind:"aggregate" + itemCode 앵커 (레코드 코드의 출처) + sources[](slug 또는 uuid 핀) — 커밋된 레코드를 집계 (급여대장 패턴) | ✅ slug 는 쿼리 시점 재해석(월 안정) · uuid 핀은 3차 수정으로 재매핑 (선행 문서 Verified) |
🆕 2026-06-19 모델 변경 — 컬럼 ↔ 수당/공제 관계 (필독)
커밋 게이트와 금액 분류 기준이 항목코드(itemCode) FK 를 레코드에 직접 스탬프하던 방식에서 (targetSheetSlug, targetColumnKey) 컬럼 포인터로 바뀌었다. 본 문서의 옛 allowanceItemCodeId 중심 서술(특히 "수식 컬럼도 commitToKind+allowanceItemCodeId 를 달면 커밋")은 이 절을 기준으로 읽는다 — 수식 커밋 컬럼도 동일하게 commitTargetSheetSlug/commitTargetColumnKey 페어가 필요하다.
- 커밋 컬럼 (수식 포함):
commitToKind("allowance"|"deduction") +commitTargetSheetSlug+commitTargetColumnKey— 이 커밋이 들어갈 급여대장 aggregate 컬럼을 (시트 slug, 컬럼 key)로 가리킨다. 수식 컬럼이어도 동일하며,inputSnapshot.formula에 수식 원문이 함께 기록된다. - 레코드: AllowanceRecord/DeductionRecord 는 더 이상
allowanceItemCodeId/deductionItemCodeId를 들지 않는다 (migration 1815 target 컬럼 추가 + 백필 → 1816 옛 FK 컬럼 DROP). 커밋 시targetSheetSlug+targetColumnKey스탬프, 업서트 키는(sourceSheetId, sourceSheetRowId, sourceSheetColumnKey). - 항목코드(eCount A/D 코드)는 급여대장 aggregate 컬럼에 산다 —
kind:"aggregate"+allowanceItemCodeId. 레코드 코드는 export 시점에(targetSheetSlug, targetColumnKey)로 그 aggregate 컬럼에서 해석한다. - 레거시 브리지: 2026-06-19 이전 커밋 컬럼(itemCode FK 만, target 페어 없음)은 commit·export 가 그 itemCode 로 같은 코드의 급여대장 aggregate 컬럼을 찾아
(slug, key)를 유도 — 옛 시트도 계속 동작. - eCount export 변경: 선택적
sheetViewId로 한 급여대장에만 스코프 가능(없으면 기간 전체 합산 — 중복 급여대장이 같은 코드 공유 시 합계 부풀던 버그 원인). 급여대장 미앵커 레코드는excludedRecords로 제외. 한 기간에 급여대장 2개째 생성 시 confirm 경고.
수식 평가 규칙 (검증 시 기대값 계산에 필요)
- 평가 순서: 비수식 컬럼(identity/ref/computed/aggregate) 먼저 → 수식은 최대 5 패스 안정화 루프 (수식→수식 참조 가능, 순환·6단 이상 체인은 null) (resolver.ts
FORMULA_PENDING). - null 전파: 기본은 참조 결측 → null. 단
format:"number"수식 컬럼은 결측을 0 으로 취급 (커밋용 금액 컬럼은 number 권장). 0 나누기 = 0 (에러 아님). - 커밋 가능 kind: literal·ref·formula·computed ✅ / aggregate·identity ❌ — 수식 컬럼도
commitToKind+commitTargetSheetSlug/commitTargetColumnKey를 달면 커밋되며 (옛allowanceItemCodeId는 레거시 브리지),inputSnapshot에{ formula: "<식>" }이 기록된다 (compute.tspick). - 함수:
ROUND/ROUNDUP/ROUNDDOWN(v,[d])·FLOOR/CEILING/ABS·MIN/MAX/SUM/AVERAGE/COUNT·IF(c,t,f)·AND/OR/NOT·TODAY()· 비교연산(> < >= <= == !=→ 1/0). 날짜 문자열은 일수로,HH:MM - HH:MM은 시간수로 자동 변환. 수식 최대 2000자. - ⚠️ 1-part cross-sheet 참조는 SUM 이 아니다 — 같은 직원의 행 중 "값이 있는 첫 행" 하나를 읽는다. 직원당 여러 행인 시트(overtime_all 등)에서 합계가 필요하면 ①(computed 모듈)이나 ④(aggregate)를 쓴다. 1-part 는 직원당 1행 시트(기준값 시트 등) 참조 전용으로 설계할 것.
UI 경로
2026-09-22 정리: 아래 경로는 과거 검증 당시의 UI입니다. 참고용으로 남아 있던 옛 모달은 삭제했으며, 현재 화면은 열 설정 드로어와 급여 반영 드로어로 나뉩니다.
컬럼 헤더 우클릭 → "🔧 열 종류 설정" → "수식 (계산)" 선택 → "편집" 팝오버 (당시 열 설정 모달): 다른 컬럼 헤더 클릭으로 [colKey] 삽입, cross-sheet 빌더로 [sheetId:colKey] 삽입. 같은 모달에서 "이 열을 수당 레코드로 커밋" 체크 + 대상 급여대장 컬럼 지정 (항목코드는 그 급여대장 aggregate 컬럼에 달려 있음).
전체 흐름
■ PHASE A — 사전 준비
1. org SHEETRH 재사용 (직원·시급계약·항목코드·6월 승인 연장근무 확인)
■ PHASE B — M월(2026-06): 수식 시트 구성 + 정산
2. 시스템 템플릿 적용: 연장근무 내역 전체(overtime_all)
3. 사용자 시트 2개:
"수당 기준값" — 직원당 1행, literal(직책수당·야간식대단가) ← cross-sheet 참조 대상
"연장수당 계산" — ①computed(모듈) + ②row-local + ③cross-sheet 수식 + 커밋 컬럼
4. 6월 데이터 → runCompute → 기대값 대조 → ✓커밋 → 급여대장(④aggregate) 확인
■ PHASE C — M+1월(2026-07): 이월 + 재계산
5. ⚠️ 순서 주의: 시스템 템플릿 재적용 먼저 (재매핑 대상 시트가 존재해야 함)
6. carry-forward (사용자 시트 2개 일괄) → 🆕 수식 uuid 재매핑 검증
7. 7월 데이터 → runCompute → 7월 기대값 → ✓커밋
■ PHASE D — 격리·정확성 검증
8. 6월 불변 · 수식-원본-레코드 일치 · 재커밋 idempotent · 회귀 체크PHASE A — 사전 준비
STEP 1 — 조직·데이터 확인
선행 문서 Verified 실행의 SHEETRH 를 그대로 쓴다 (없으면 선행 문서 STEP 1 수행). 확인 사항:
- [ ] 직원 2명 이상에 활성 근로계약 + 시급(hourlyRate) — identity
[hourlyRate]참조와 모듈 계산의 입력 - [ ] 수당 항목코드 2개 이상 (예:
13야간근무수당,14연장근무수당 — STEP 3 커밋 컬럼이 참조) - [ ] 6월 승인된 연장근무 2~3건 (평일 + 야간 섞어서) + PENDING 1건 (집계 제외 검증용)
PHASE B — M월(2026-06)
STEP 2 — 시스템 템플릿 적용
sheetTemplate.applyToPeriod 로 연장근무 내역 전체(slug overtime_all) 를 2026-06 에 적용. (선행 문서에서 검증된 경로 — 여기서는 데이터 소스 역할만.)
✅ 검증
- [ ] 6월 overtime_all 시트에 승인 건 + PENDING 건 모두 표시됨 (표시는 전 상태 — 설계)
STEP 3 — 수식 수당 시트 구성 (핵심)
둘 다 visibility:"SHARED" (STEP 6 carry-forward 대상), rowBasis employee.
3-1. "수당 기준값" 시트 — cross-sheet 참조 대상 (직원당 1행)
| key | kind | 비고 |
|---|---|---|
emp | identity | 성명 |
positionAllowance | literal · number | 직책수당 (예: A 직원 30,000 / B 직원 0) |
nightMealUnit | literal · number | 야간식대 단가 (예: 8,000) |
3-2. "연장수당 계산" 시트 — 참조 방식 ①②③ 집약
| key | kind | 구성 | 참조 방식 |
|---|---|---|---|
emp | identity | 성명 | — |
rate | identity | refField hourlyRate | — |
wd | computed | module overtime_record · key weekday_base | ① |
nt | computed | module overtime_record · key night_base | ① |
ntDays | computed | module overtime_record · key night_days | ① |
posAlw | formula · number | [<수당기준값ID>:positionAllowance] | ③ 1-part |
ntMeal | formula · number | [<수당기준값ID>:nightMealUnit] * [ntDays] | ③+② |
otPay | formula · number · 커밋 | ROUND([wd] + [nt] + [ntMeal], 0) + commitToKind:"allowance" + 대상 급여대장 aggregate 컬럼(연장근무수당 코드) — 이 target 페어가 커밋을 구동 | ② |
posPay | formula · number · 커밋 | IF([posAlw] > 0, [posAlw], 0) + 대상 급여대장 aggregate 컬럼(직책수당 계열 코드) — target 페어가 커밋 구동 | ② |
수식 안의
<수당기준값ID>는 cross-sheet 빌더가 삽입하는 6월 시트의 uuid — STEP 6 의 재매핑 검증 대상이 바로 이 토큰이다.
✅ 검증
- [ ] 수식 컬럼에 커밋 설정이 저장됨 (
sheetView.get으로 columnLayout 회독) - [ ] 수식 에디터에서 컬럼 헤더 클릭 →
[colKey]삽입, cross-sheet 빌더 →[sheetId:colKey]삽입 동작
STEP 4 — 6월 계산 → 커밋
- "수당 기준값"에 직원별 literal 입력 (A: 직책수당 30,000 · 야간식대 8,000 / B: 0 · 8,000).
- "연장수당 계산"
runCompute→ 기대값 대조. 기대값 예시 (A 직원: 시급 10,000 · 승인 평일연장 4h · 승인 야간 2h(1일) · PENDING 5h):
| 컬럼 | 계산 | 기대값 |
|---|---|---|
wd | 10,000 × 4h × 1.5 | 60,000 |
nt | 10,000 × 2h × 0.5 | 10,000 |
ntDays | 야간 근무일수 | 1 |
ntMeal | 8,000 × 1 | 8,000 |
otPay | ROUND(60,000+10,000+8,000) | 78,000 |
posPay | IF(30,000>0,…) | 30,000 |
commitToRecords→ AllowanceRecord 2종(otPay·posPay) 생성.- 급여대장(시스템 템플릿)에 ④aggregate 로 항목코드별 합계 노출 확인.
✅ 검증
- [ ] PENDING 5h 가
wd/otPay에 미반영 (승인 게이트 — 그 건을 승인하면 재계산 시 반영됨도 확인) - [ ] 수식 5-pass:
ntMeal(수식)을 참조하는otPay(수식) 정상 평가 (수식→수식 체인) - [ ] 커밋 레코드: 금액 78,000/30,000 ·
targetSheetSlug/targetColumnKey스탬프 (항목코드는 대상 급여대장 aggregate 컬럼) ·inputSnapshot.formula= 수식 원문 - [ ] B 직원
posPay= 0 → 0원 레코드 생성 여부 기록 (선행 문서 미해결 질문 — eCount 는 0원 행 미출력) - [ ] 재커밋 →
recordsUpdated>0, recordsInserted=0(idempotent) - [ ] 6월 레코드 수·합계 기록 (STEP 8 비교 기준)
PHASE C — M+1월(2026-07)
STEP 5 — ⚠️ 시스템 템플릿 재적용 먼저
2026-07 에 overtime_all 재적용 (slug idempotent). 순서가 중요하다: 재매핑은 "같은 이름의 시트가 대상 기간에 이미 있어야" 성립 — 수식/aggregate 가 시스템 시트를 직접 참조하는 구성이라면, carry-forward 전에 시스템 시트부터 만들어야 한다. (본 시나리오의 수식은 사용자 시트만 참조하므로 in-batch 매핑으로도 충분하지만, 순서 원칙 자체를 검증 항목으로 둔다.)
STEP 6 — carry-forward + 🆕 수식 uuid 재매핑 검증
sheetView.carryForward { from: 2026-06 → to: 2026-07 } — "수당 기준값"과 "연장수당 계산"이 같은 배치로 복사된다 (in-batch fromId→toId 맵 우선).
✅ 검증 (4차 수정의 1차 검증)
- [ ] 7월 "연장수당 계산"의
posAlw/ntMeal수식 문자열에서[<uuid>:...]가 7월 "수당 기준값" 시트의 uuid 로 재작성됨 (sheetView.get으로 formula 원문 비교 — 6월 uuid 가 남아 있으면 버그) - [ ] (경로 추가 검증) 6월 "연장수당 계산"을
saveSheetAsTemplate→ 7월에applyToPeriod, 그리고 JSONexportTemplate→importTemplate— 세 경로 모두 재매핑됨 (템플릿/JSON 경로는 in-batch 맵이 없어 이름 DB 조회로 해석: 7월에 동명 "수당 기준값"이 이미 있어야 함) - [ ] (음성 케이스) "수당 기준값"을 7월로 가져오기 전에 "연장수당 계산"만 단독 템플릿 적용 → uuid 미해석 시 원본(6월) uuid 유지 (드롭 아님) → 이후 컬럼 에디터에서 재선택 가능
- [ ] computed 컬럼(
wd/nt)·커밋 설정은 그대로 복사 (선행 문서 검증과 동일)
STEP 7 — 7월 재계산 → 커밋
- 7월 승인 연장근무 입력 (6월과 다른 시간으로 — 예: A 평일 2h만). "수당 기준값" 7월 시트에 literal 재입력 (행/값은 복사되지 않음 — 선행 문서 전제).
runCompute→ 7월 기대값 (A: wd=30,000 · nt=0 · ntMeal=0 → otPay=30,000) →commitToRecords.
✅ 검증
- [ ] 수식이 7월 데이터만으로 재계산 (6월 값 잔존 없음 — 특히 cross-sheet 참조가 7월 기준값 시트의 새 입력을 읽는지)
- [ ] 7월 AllowanceRecord lineage = 7월 시트 id
- [ ] 급여대장(7월) aggregate 가 7월 커밋만 집계
PHASE D — 격리·정확성
STEP 8 — 최종 검증
- [ ] 6월 불변: 레코드 수·합계 STEP 4 기록과 동일 · 6월 시트 수식 uuid 는 여전히 6월 기준값 시트
- [ ] 6월 시트 열람 시 6월 값 그대로 재현 (수식 재평가가 기간 스코프를 지킴)
- [ ] (회귀) 수식 컬럼 셀 직접 편집 시도 → "수식/계산으로 만들어진 값" 토스트 + 서버 400 (선행 문서 편집 정책)
- [ ] (회귀) 동명 시트가 7월에 2개일 때(템플릿 재적용으로 중복 생성) 재매핑이 earliest-created 로 결정적 매핑
부록 A — 함정 요약
| # | 함정 | 회피/비고 |
|---|---|---|
| 1 | 1-part cross-sheet = "값 있는 첫 행" 단일 조회 (SUM 아님) | 직원당 1행 시트만 1-part 로 참조. 합계는 computed 모듈/aggregate |
| 2 | 재매핑은 이름 기준 | 참조 대상 시트가 대상 기간에 먼저 존재해야 함 (STEP 5 순서) · 시트 이름을 바꾸면 다음 이월에서 해석 실패(원본 uuid 유지) |
| 3 | 미승인(PENDING) 연장은 모듈 집계 제외 | 표시는 되지만 금액 0 — 기대값 계산 시 승인 건만 |
| 4 | format:"number" 미지정 수식 | 참조 결측 시 0 이 아니라 null → 커밋 skip (rowsSkippedDueToMissingAmount) — 금액 수식은 number 필수 |
| 5 | 수식 체인 5단 초과·순환 | null — 깊은 체인은 중간 컬럼으로 분해 |
| 6 | 동명 시트 중복 | earliest-created 매핑 (slugless 재적용이 중복을 만든 경우 — 선행 문서 함정 4) |
부록 B — 빠른 호출 순서 (스모크)
# M월
sheetTemplate.applyToPeriod (overtime_all, 2026-06)
sheetView.create ×2 (수당 기준값 · 연장수당 계산 — formula+commit 컬럼, SHARED)
(기준값 literal 입력 · 연장 승인) → sheetView.runCompute → 기대값 대조 → commitToRecords
# M+1월
sheetTemplate.applyToPeriod (overtime_all, 2026-07) # ⚠️ 먼저
sheetView.carryForwardPreview → carryForward (06→07)
sheetView.get ×2 → formula 원문에서 uuid 재매핑 확인 # 🆕 4차 수정 검증
(7월 데이터) → runCompute → commitToRecords
(회귀) saveSheetAsTemplate→applyToPeriod / exportTemplate→importTemplate 재매핑 동일 확인부록 C — 미해결 / 확인 필요
- [x] 수식 cross-sheet 참조가 stale 상태로 읽힐 때의 동작 (재매핑 실패 시) — 실측 (2026-06-11): 전월 시트 값을 조용히 그대로 읽는다 (8월 음성 케이스에서 posAlw = 6월 기준값 30,000, null 아님). 조용한 오답 경로이므로 참조 대상 시트를 먼저 만드는 순서(STEP 5)가 운영 수칙. 장기적으로는 stale ref 감지 시 UI 경고 배지 검토 가치 있음.
- [ ] 수식 에디터에 함수 목록/문법 도움말이 없음 (
formulaFunctionCatalog는 코드에만 존재) — 사용자 가이드 필요 여부. - [ ] cross-sheet 참조를 uuid 대신 slug/이름 기반 토큰으로 쓰는 방안 — 재매핑 자체가 불필요해짐 (장기 개선 후보).
- [ ] 0원 수식 커밋 정책 (선행 문서와 공통 — eCount 출력에는 영향 없음).
부록 D — BESTRH 실데이터 Excel 미러 실측 (2026-06-11 추가 실행)
non-password.xlsx 의 계산을 BESTRH(베스트재활병원, yongku.lee 계정) 실데이터 위에서 시트로 재현·대조한 결과:
- 시간외 수당 (행정시간외 탭): Excel L열 =
ROUNDDOWN(연장시간 × 시급 × 1.5, -1)(10원 내림, 일별). 1월 "행정 시간외수당 (엑셀대조)" 시트에 김진호 5일치 일별 행 + cross-sheet 시급 참조([시급기준:rate]— Excel VLOOKUP 미러) + 동일 수식 → 5일 전부 Excel 캐시값과 1원 단위 일치 (35,970/23,980/95,940/35,970/71,950 · 합 263,810). 야간수당(M열 ×0.5) 패턴도 동일 구성. - 치료실 인센티브 (재활 탭): Excel = Σ(종목 건수 × 단가) = 청구금액, 병원수익 = ÷2. 치료 단가 시트 부트스트랩으로 Excel 단가 8종 등록(ratio 0.5) →
upsertTreatmentSession에 Excel 첫 행 건수 입력 → totalBilling 1,893,817 = Excel 청구금액 J4 정확 일치, hospital 946,909(=J/2), 치료사 인당 473,454 fan-out. 치료일 인센티브 시트 동기화로 billed/hospital/therapist 표시 확인. - 매니저 열람: yongku.lee 계정으로 1월 기간에서 imported 시트들(급여대장(전체)·공제내역·특별수당 등)과 함께 위 시트들 열람 가능, incentive_all(1월) 346건 materialize. 5월 출퇴근 내역 전체(attendance_all) 시트에 수당 수식 컬럼을 추가하는 것도 차단 없음.
실측에서 드러난 갭 → 2026-06-11 2차에서 수정:
- 🐛 (수정) timestamp ref 컬럼 간 산술 = epoch ms —
[actualCheckOut] - [actualCheckIn]이 시각이 아니라 epoch 밀리초로 계산되던 버그. resolver 의 수식 값 강제변환에 Date/ISO-datetime → 소수 일수(day fraction) 분기 추가 (resolver.ts) → Excel(퇴근-출근)*24가 정확히 시간으로 평가됨. 실측: 김진호 5/20 22:16~익일 12:06 → workH 13.84h, otH 4.84h (야간 wrap 포함). - 🐛 (수정) 치료 단가
codewrite-back 불가 — 두 가지 근본 원인: ① 시드 템플릿code컬럼에treatmentField태그 부재, ②treatmentFieldzod enum 에"code"가 빠져sheetView.update가 태그를 strip (기존 시트 수동 태깅도 불가했던 진짜 버그). seed + 스키마 enum +applyTreatmentWriteback의code브랜치까지 추가 → 실측: 치료 단가 시트에서code입력 →treatment_items.code갱신 → 세션 동기화 tally 정상 (billed 1,893,817 = Excel 일치). - 🐛 (수정) number 포맷 컬럼이 소수(1.5h)를 정수(2)로 반올림 표시하던 버그 —
formatAmount가 정수는 그대로, 소수는 2자리까지 유지하도록 변경 (render.ts). 실측: 연장근무계 컬럼이 1.5 로 표시 (계산값은 원래 정확, 표시만 수정). - ℹ️ 치료일 인센티브 동기화는 treatmentcount 컬럼의
treatmentCategoryId바인딩이 선행돼야 함 (시드 직후엔 미설정 →sessions:0) — 의도된 동작, 실측 확인.
갭② — 2026-06-11 4차에서 backfill 로 해소:
- 🐛 (수정 — 데이터 backfill) 시컴 지문 근태의 야간 실근무가 시트에 안 잡히던 문제. 최종 진단: 코드(buildScheduledTimes/finalizer/timezone)는 전부 정상. 원인은 imported 데이터의 시프트 배정 — 야간/교대 근무자가 전부 기본 "일반근무(주간 09-18)"로 배정돼, scheduled-window 기준 산정(설계: 지문은 출결 증빙용)이 주간 창을 슬라이스 → night 0. 해소:
- backfill-attendance-shifts.ts — 행 단위로 actual 체크인 시각에 원형거리 최근접 시프트를 추론(3교대는 일자별 가변이므로 직원 단위가 아닌 행 단위),
buildScheduledTimes재생성 +finalizeAttendanceHours재계산. dry-run 기본 /--apply/ 확정·지급 급여 기간 제외 가드 / 최근접 거리 3h 초과는 저신뢰로 보존. - BESTRH 실행 결과: 6,323건 스캔 → 3,515건 반영 (확정 6월 1,242건 보호, 출/퇴근 누락 431건 제외). 시프트 전환: 일반근무→주간3교대 2,391 · →오후3교대 648 · →야간3교대 116 등. night 합계 415h → 1,962h 회복, 5월 출퇴근 시트에 야간>0 행 751건 표시. 재실행 시 변경 0건 (idempotent).
- 잔여(운영 확인 목록으로 출력): 저신뢰 271건 (21시 전후·새벽 4~5시 체크인 — 시프트 정의에 없는 출근대), 스케줄 중복 충돌 4건 (동일 직원·동일 창에 행 2개 = 중복 적재 의심).
- 연장근무 수당은 설계대로 OvertimeRecord(신청→승인) 경로 — 이 backfill 은 출퇴근 시트의 야간/주말 시간 표시를 바로잡는 것.
- backfill-attendance-shifts.ts — 행 단위로 actual 체크인 시각에 원형거리 최근접 시프트를 추론(3교대는 일자별 가변이므로 직원 단위가 아닌 행 단위),
오해 방지 안내 추가 (2026-06-11 3차) — "조용한 오답"이 생기던 지점마다 UI 고지:
- 계약 시트 manual 행 편집: 저장 전 confirm — "시트에만 저장되는 것이 아니라 실제 근로계약에 기록됩니다 (없으면 v1 생성, 있으면 새 버전)" (auto 행은 기존 generic confirm 의 라벨을 "근로계약"으로 정정).
- 치료일 인센티브 🔄 동기화: 코드 미등록 치료항목이 집계에서 빠지면 어떤 코드가 빠졌는지 toast 로 경고 (
syncTreatmentSessions가unmatchedCodes[]반환 — 실측:["GAIT"]). - 이월(carry-forward): 참조 시트가 대상 월에 없어 전월을 계속 읽게 되는 사본이 생기면 시트 이름 목록을 toast 로 경고 (
carryForward가refWarnings[]반환 — 실측:["수당 기준값 ('26.6)"]. 같은 배치로 함께 이월되면 in-batch 해석되어 경고 없음). - 출퇴근 내역 전체 템플릿 설명에 명시: "시프트 배정 직원의 시간은 스케줄 기준 산정(지문은 증빙용), 연장근무 수당은 신청·승인 내역으로 계산" — 갭② 오해(8h 캡)의 안내.
- 수식 편집 팝오버에 날짜/시각 변환 힌트 추가:
([퇴근]-[출근])*24 = 시간,"09:00 - 18:00"→ 9 등.
부록 D-2 — 근로계약 시스템 템플릿 시트 인라인 편집 (2026-06-11 신규)
importer 로 적재된 BESTRH 는 근로계약이 0건이라 수식 모듈의 시급 파생이 불가했다 (부록 D 말미). 이를 풀기 위해 계약급여내역(employment_contract_all) 시트에서 시급/기본급을 직접 편집하는 경로를 신설:
- 신규 proc
employee.upsertContract(employee.router.ts) — append-only 버전 관리(grade-promotion 패턴): 활성 계약을 supersede 하고 merge 된 v+1 생성, 계약이 없으면 v1 부트스트랩. 스키마upsertEmploymentContractSchema, SDKemployee.upsertContract. - UI 편집 핸들러 edit/employment-contract.ts — 시트의 autoPopulate source 가
employment_contract면 라우팅 (계약 행은 legacyrecordKind:"allowance"형태라 source 기준으로 분기)._saveCellEdit게이트도 계약 시트 전용 분기 추가 (auto 행 = 기존 계약 supersede, manual 행 = v1 부트스트랩 — "계약 없는 imported 조직" 케이스). - 실측 (BESTRH 5월 계약급여내역 시트):
- auto 행: 김진호 시급 → tRPC v1(15,990)→v2(16,000), UI 더블클릭+confirm 으로 v3(15,990) → DB 버전 체인 + supersededAt 정확, 시트 즉시 재반영.
- manual 행: 계약 0건이던 김영일에 "+ 행 추가" → 시급 12,400 입력 → v1 계약 부트스트랩 생성 (created:true).
- 시급이 계약에 들어가므로 이후 연장근무 computed 모듈(
overtime_record)의 시급 파생도 가능해짐.