Skip to content

사용량 입력 (계량 지침 캐논) — 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: '사용량'을 설정한다. 이 항목은:

  1. 사용량 입력 화면의 부과 항목 선택 목록에 자동 추가.
  2. CSV 가져오기 시 해당 chargeCode의 유효성 검사 통과.
  3. useAllocations.basisQuantity 분기에서 useMeterReadings().usageOf로 파생.
  4. useMetering.meteringFor가 루프로 순회하여 고지서 검침 블록 생성.

세금성격: 수도료 = 면세(taxCategory 면세), 전기료 = 과세(taxCategory 과세). 과세 항목은 VAT 및 세금계산서 경로를 자동 통과한다. 검침 입력 계층은 세금성격에 무관하게 동일 로직.


6. CSV 계약

6-1. 파일 형식

  • 인코딩: UTF-8 with BOM
  • 구분자: 쉼표(,)
  • 신규 헤더 행: 호,부과항목,당월사용량 (이 순서 — 호 우선)
  • 레거시 호,부과항목,당월지침[,전월지침,전전월지침]도 기존 파일 호환을 위해 계속 허용한다.

예시:

호,부과항목,당월사용량
101,수도료,35
102,수도료,18
101,공용설비사용료,12

6-2. 검증 규칙 (행별)

조건오류 메시지
호 칸 비어 있음호 누락
호가 units 마스터(buildSeeduseAllocations.units와 동일 소스)에 존재하지 않음등록되지 않은 호
부과항목이 활성 chargeBasis '사용량'이 아님사용량 입력 부과항목 아님

✅ W3 가드 완료(2026-07-06, 배분가드 W3-4·FB-11): 과거엔 unitCode 존재검증이 없어 "101호"(그리드 표기 그대로, 실제 코드는 "101") 같은 오기입 행이 무음 스킵되거나 고아 키로 저장되면서도 "N건 반영" 허위 성공으로 보고됐다. 행별 unitCode 존재검증 추가 — 미등록 호는 실패 리포트로 분리(errors)·성공 카운트 제외·고아 키 미생성. setReading(그리드 수동입력)은 임의 unitCode upsert 계약을 유지하기 위해 미가드(spec §5 명시 — CSV 경로만 검증). 배분 연결: 오기입 호가 무음 스킵되면 그 호의 라인 가중치가 조용히 0으로 남아 useAllocationsunallocated가 무단 발생할 수 있었다(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().readingsreactive 지침 객체 (readings[chargeCode][unitCode])
useMeterReadings().meteredChargescomputed — 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).
  • 누진요율·계절/시간대 요금.
  • 검침 이력 감사 추적·이상치 경보.