다크모드
사용량 입력 (계량 지침 캐논) — BE 참고
관리비 상용화 프로그램 #4. 설계 정본:
docs/superpowers/specs/2026-07-01-metering-input-canon-design.md.
1. 개요
사용량 기반 부과항목은 종류와 무관하게 호별 당월 사용량을 입력받아 부과금액을 결정한다. UI는 활성 chargeBasis === '사용량' 항목을 선택하게 하고, 입력된 사용량을 기존 누적 지침 캐논으로 변환한다. 본 문서는 데이터 모델·캐논 사슬·CSV 계약·불변식을 기술한다.
2. 데이터 모델
2-1. 진실원 (Single Source of Truth)
useMeterReadings.js 모듈 레벨 reactive 싱글톤이 계량 지침 마스터를 소유한다.
키 구조: readings[chargeCode][unitCode]
readings = {
'a1b1c2': { // 수도료 (면세)
'101': { twoMonthsAgo: 100, prevMonth: 130, currentMonth: 165 },
'102': { twoMonthsAgo: 200, prevMonth: 222, currentMonth: 240 }
},
'a1b1c4': { // 전기료 (과세)
'101': { twoMonthsAgo: 800, prevMonth: 850, currentMonth: 910 },
'102': { twoMonthsAgo: 600, prevMonth: 640, currentMonth: 680 }
}
}- 그레인:
(chargeCode, unitCode)쌍. 값은 누적 지침{ twoMonthsAgo, prevMonth, currentMonth }(m³ 또는 kWh 등 단위 불문). - 모듈 로드 시
buildSeed(DEMO_CONFIG).meterReadings를 deep-clone 초기화 (회귀0 보장).
2-2. 마이그레이션 이력
구 demoSeed.meterReadings 구조: { unitCode: {...} } (수도 암묵 1층)
신 구조: { chargeCode: { unitCode: {...} } } (부과항목-호 2층)
수도 지침은 chargeCode 'a1b1c2' 아래로 이동하였으며 값은 불변이다.
2-3. 파생 사용량
usageOf(chargeCode, unitCode)=max(0, currentMonth − prevMonth)prevUsageOf(chargeCode, unitCode)=max(0, prevMonth − twoMonthsAgo)- 음수 클램프: 계량기 교체 등으로 지침이 감소할 때 사용량을 0으로 처리한다.
2-4. 계량 유틸리티 집합
사용량 대상 부과항목 = useCharges.charges 중 정규화한 chargeBasis === '사용량'인 것 전체. 화면의 선택 목록은 그중 활성 항목인 usageEntryCharges만 사용한다. 부과항목 마스터에서 사용량 항목을 추가하면 선택 목록·CSV 가져오기에 자동 편입된다. 수도·전기 하드코딩은 없다.
3. 캐논 사슬 (Canon Chain)
호별 당월 사용량 입력
↓ setUsage: currentMonth = prevMonth + usage
↓
usageOf(chargeCode, unitCode) = max(0, currentMonth − prevMonth)
↓
useAllocations.basisQuantity(line, '사용량') → useMeterReadings().usageOf(line.chargeCode, line.unitCode)
↓
lineWeight = basisQuantity × dayRatio (occupancyDays / monthDays)
↓
수도료/전기료 pool × lineWeight 비례 배분 → 세대별 부과금액
↓
청구·수납·고지서·미수금연령분석·분개 자동 전파
↓
useMetering → 고지서 검침/비교 블록 표시 (reactive 재배선)3-1. 점유분배 금지 — dayRatio가 이미 시간분배 처리
계량기는 호 단위 1개다. 한 호에 여러 점유 라인이 있을 때(순차 점유·공동 점유) 각 라인은 호 전체 당월사용을 basisQuantity로 갖고, dayRatio(= occupancyDays / monthDays)가 시간분배를 담당한다.
basisQuantity에 점유 계수를 별도로 곱하면 dayRatio와 이중 분배가 발생하여 금액이 틀어진다 — 금지.
데모 시드 확인 (101호 수도, L4·L6):
- c-hong (occupancyDays 15): usage = 35, lineWeight = 35 × (15/30) = 17.5
- c-kim (occupancyDays 15): usage = 35, lineWeight = 35 × (15/30) = 17.5
- 합 = 35 → 호 전체 사용량과 일치. 점유 계수 추가 불필요.
4. 회귀0 불변식
핵심 불변식: 시드 초기화 직후 useMeterReadings().usageOf(line.chargeCode, line.unitCode) = 시드 line.basis.usage.
보장 근거:
- 데모 시드 지침은 모두 양수 증가 시퀀스 → max(0) 클램프가 값을 바꾸지 않는다.
- 수도 101호: prevMonth 130, currentMonth 165 → usageOf = 35 = L4·L6 basis.usage.
- 배분 공식·dayRatio·잔차흡수(I1) 전부 불변 → 배분금액 byte-identical.
이 불변식을 위반하면 기존 vitest(useAllocations·useMetering·MeteringBlocks·_seedExpect·invoicing)가 실패한다.
5. 부과항목 연결 (#2 마스터와 관계)
계량 부과항목 등록 시 chargeBasis: '사용량'을 설정한다. 이 항목은:
- 사용량 입력 화면의 부과 항목 선택 목록에 자동 추가.
- CSV 가져오기 시 해당
chargeCode의 유효성 검사 통과. useAllocations.basisQuantity분기에서useMeterReadings().usageOf로 파생.useMetering.meteringFor가 루프로 순회하여 고지서 검침 블록 생성.
세금성격: 수도료 = 면세(taxCategory 면세), 전기료 = 과세(taxCategory 과세). 과세 항목은 VAT 및 세금계산서 경로를 자동 통과한다. 검침 입력 계층은 세금성격에 무관하게 동일 로직.
6. CSV 계약
6-1. 파일 형식
- 인코딩: UTF-8 with BOM
- 구분자: 쉼표(
,) - 신규 헤더 행:
호,부과항목,당월사용량(이 순서 — 호 우선) - 레거시
호,부과항목,당월지침[,전월지침,전전월지침]도 기존 파일 호환을 위해 계속 허용한다.
예시:
호,부과항목,당월사용량
101,수도료,35
102,수도료,18
101,공용설비사용료,126-2. 검증 규칙 (행별)
| 조건 | 오류 메시지 |
|---|---|
| 호 칸 비어 있음 | 호 누락 |
호가 units 마스터(buildSeed — useAllocations.units와 동일 소스)에 존재하지 않음 | 등록되지 않은 호 |
부과항목이 활성 chargeBasis '사용량'이 아님 | 사용량 입력 부과항목 아님 |
✅ W3 가드 완료(2026-07-06, 배분가드 W3-4·FB-11): 과거엔 unitCode 존재검증이 없어 "101호"(그리드 표기 그대로, 실제 코드는 "101") 같은 오기입 행이 무음 스킵되거나 고아 키로 저장되면서도 "N건 반영" 허위 성공으로 보고됐다. 행별 unitCode 존재검증 추가 — 미등록 호는 실패 리포트로 분리(
errors)·성공 카운트 제외·고아 키 미생성.setReading(그리드 수동입력)은 임의 unitCode upsert 계약을 유지하기 위해 미가드(spec §5 명시 — CSV 경로만 검증). 배분 연결: 오기입 호가 무음 스킵되면 그 호의 라인 가중치가 조용히 0으로 남아useAllocations의unallocated가 무단 발생할 수 있었다(docs/DESIGN-BILLING-CHARGE-NORMALIZATION-SC2-2026-06-17.md§11-4) — 입력 단계 검증으로 근본 차단.
비숫자 당월지침 주의: 당월지침이 숫자가 아닌 경우는 오류가 아니라
num()헬퍼가 0으로 무음 처리한다 (errors목록 미포함). 향후 숫자 검증 강화 시 이 항목을 오류 계약에 추가할 수 있다.
- 헤더 자동 감지: 첫 행에 '호'가 포함되면 건너뜀.
- 부분 성공: 오류 행은 건너뛰고 나머지는 정상 입력. 결과
{ added: N, errors: [{line, reason}] }반환. line번호는 헤더 포함 1-indexed.
6-3. upsert 동작
setReading(chargeCode, unitCode, patch): 기존 레코드에 patch를 병합. twoMonthsAgo·prevMonth·currentMonth 중 제공된 필드만 갱신.
7. API 요약
| 함수 | 설명 |
|---|---|
useMeterReadings().readings | reactive 지침 객체 (readings[chargeCode][unitCode]) |
useMeterReadings().meteredCharges | computed — chargeBasis '사용량' 부과항목 배열 |
useMeterReadings().usageOf(cc, uc) | 당월사용 = max(0, currentMonth − prevMonth) |
useMeterReadings().prevUsageOf(cc, uc) | 전월사용 = max(0, prevMonth − twoMonthsAgo) |
useMeterReadings().setReading(cc, uc, patch) | 지침 upsert |
useMeterReadings().setUsage(cc, uc, usage) | 직접 사용량을 누적 당월지침으로 변환해 저장 |
useMeterReadings().usageEntryCharges | 활성 상태인 사용량 입력 항목 배열 |
useMeterReadings().importCsv(text) | CSV 일괄 import → {added, errors} |
useMeterReadings().resetToSeed() | 데모 시드로 초기화 |
8. 스코프 경계
포함: 계량 지침 마스터·usageOf 파생·캐논 사슬·CSV import·수도(면세)+전기(과세) 데모.
제외 (YAGNI):
- 계량기 마스터(계기번호·검침원 배정).
- 자동 원격검침(AMR).
- 누진요율·계절/시간대 요금.
- 검침 이력 감사 추적·이상치 경보.