Skip to content

사용량 입력 — FE 메인테이너 참고

관리비 상용화 프로그램 #4. 설계 정본: docs/superpowers/specs/2026-07-01-metering-input-canon-design.md.


1. 라우트

경로설명
/service-charge/metering/generalsrc/views/service-charge/metering/general/IndexView.vue사용량 입력 메인 화면

2. 컴포넌트 트리

IndexView.vue
├── HeaderOrg.vue           — 페이지 제목·프로퍼티·기준월 표시 (disclosure-page-header)
└── MainOrg.vue             — provide('meteringState', {saveTrigger, isDirty})
    ├── blocks/DataToolBarOrg.vue     — 선택 항목용 [템플릿 다운로드]·[가져오기]·[저장]
    ├── blocks/MeteringGridOrg.vue    — 사용량 항목 선택 + 선택 항목의 호별 사용량 그리드
    └── overlays/SheetImportMeteringArt.vue  — CSV import 시트 (sheet-width-3xl)

모두 src/components/service-charge/metering/general/ 아래에 위치한다.


3. 소비 컴포저블

3-1. useMeterReadings (신규 — 진실원)

src/composables/useMeterReadings.js

  • 모듈 레벨 reactive 싱글톤 (useCharges, useInitialReceivables 동형).
  • readings ref: readings[chargeCode][unitCode] = { twoMonthsAgo, prevMonth, currentMonth }.
  • 모듈 로드 시 buildSeed(DEMO_CONFIG).meterReadings deep-clone 초기화 → 회귀0.
  • meteredCharges: computed — useCharges().charges 중 정규화한 chargeBasis === '사용량'.
  • usageEntryCharges: computed — meteredCharges 중 활성 항목. 화면 Select의 유일한 데이터원.
  • usageOf(cc, uc) = max(0, currentMonth − prevMonth); prevUsageOf(cc, uc) = max(0, prevMonth − twoMonthsAgo).
  • setReading(cc, uc, patch): upsert.
  • setUsage(cc, uc, usage): currentMonth = prevMonth + usage로 변환해 누적 지침 캐논에 저장.
  • importCsv(text): CSV 파싱·행별 검증·부분 성공 → {added, errors}.
  • resetToSeed(): 시드 복원.

3-2. useAllocations — basisQuantity 재배선

src/composables/useAllocations.js 55–57행:

js
// 사용량 basisQuantity: 계량 지침에서 파생. 점유분배 금지(dayRatio가 이미 시간분배).
if (chargeBasis === "사용량" || chargeBasis === "USAGE")
  return useMeterReadings().usageOf(line.chargeCode, line.unitCode);

기존 line.basis.usage(정적)를 useMeterReadings().usageOf()(파생)로 교체. 배분 공식·dayRatio·잔차흡수는 불변. 회귀0 불변식: 시드 초기화 직후 파생값 == 시드 line.basis.usage.

3-3. useMetering — 다utility 일반화

src/composables/useMetering.js

  • readings 접근을 useMeterReadings().readings(reactive)로 재배선 → 검침 입력이 고지서 표시에 reactive 전파.
  • meteringFor(selection): meteredCharges를 루프하여 각 계량 부과항목별 usage/comparison 행 생성. 수도 단일 하드코딩 → N utility 일반화.
  • UTILITY_PARAMS: chargeCode 키 맵으로 prevPoolFactor·designatedUnitPrice 보관. 미등록 유틸은 DEFAULT 폴백.

4. 그리드 draft 관리

MeteringGridOrg.vue가 draft를 관리한다.

4-1. 구조

draft.value = { [chargeCode]: { [unitCode]: string } }
  • MainOrgselectedChargeCode를 소유하고 활성 사용량 항목의 첫 항목을 기본 선택한다.
  • MeteringGridOrg의 Select는 usageEntryCharges만 표시한다. 수도·전기 목록을 하드코딩하지 않는다.
  • initDraft(): 각 항목·호의 usageOf()를 당월 사용량 문자열로 초기화한다.
  • 표는 선택한 항목의 호별 행만 렌더한다. 항목을 바꿔도 다른 항목의 미저장 draft는 유지한다.

4-2. dirty 게이트

localIsDirty: draft 숫자값 !== usageOf(cc, uc)인 셀이 하나라도 있으면 true. provide('meteringState').isDirty로 전파 → DataToolBarOrg [저장] 활성화.

4-3. 저장

save(): dirty 셀만 setUsage(cc, uc, usage) 호출한다. 누적 지침 변환은 컴포저블이 담당하며 화면은 직접 사용량만 다룬다.

4-4. readings watch 동기화

js
watch(
  readings,
  () => {
    initDraft();
  },
  { deep: true },
);

importCsv나 외부 setReading 호출 후 readings가 변경되면 draft를 재초기화한다. 저장 전 draft 편집 내용은 덮어쓰인다 (프로토타입 수용 동작).


5. DataToolBarOrg — 저장 트리거 패턴

MainOrg.vueprovide('meteringState', { saveTrigger: ref(0), isDirty: ref(false) })를 제공한다.

  • DataToolBarOrg: [저장] 클릭 시 saveTrigger.value++.
  • MeteringGridOrg: watch(saveTrigger, (newVal, oldVal) => { if (newVal > oldVal) save() }).

직접 emit 대신 provide/inject + 카운터 증가로 형제 컴포넌트 간 통신 → 컴포넌트 커플링 최소화.


6. CSV 가져오기 시트

SheetImportMeteringArt.vue — CLAUDE.md §0 집중폭 규칙에 따라 sheet-width-3xl.

  • <dialog> popover 패턴 (id="sheet-import-metering-art", closedby="any").
  • 파일 선택 (FileReader.readAsText(..., 'utf-8')) + 텍스트 붙여넣기 textarea 병행.
  • [가져오기 실행]importCsv(text.value)result.value = {added, errors} 표시.
  • 닫을 때 @close="handleClose" → text·result 초기화.

7. 템플릿 다운로드

DataToolBarOrg.downloadTemplate():

  • BOM(UTF-8) + 헤더 호,부과항목,당월사용량 + 선택 항목 예시 행.
  • Blob → 가상 <a> 클릭 → 사용량입력_{항목명}_템플릿.csv 다운로드.

8. i18n 키

src/i18n/locales/ko.jsonbilling.serviceCharge.meteringPage.*:

값 예시
title사용량 입력
chargeItem부과 항목
columns.unit
columns.usage당월 사용량
import.template템플릿 다운로드
import.open가져오기
import.pasteCSV 파일 선택
import.run가져오기 실행
import.resultN건 추가, M건 실패 (메시지)

en.json 패리티(2026-07-10): 위 meteringPage.* 11키를 포함해 en.json에 실측 97개 leaf 키가 누락돼 있었다(감사 추정 "113"은 상향 조정 전 근사치 — 실제 leaf-diff 계측이 정본). 전량 보충 완료(11ef3cd9b). 회귀 가드: src/i18n/__tests__/localeParity.spec.jsko.json(정본)의 모든 leaf 키가 en.json에도 존재하는지 매 실행마다 검증(누락 시 fallback ko 텍스트가 en 사용자에게 노출되는 것을 차단). 새 i18n 키를 추가할 때는 ko·en 양쪽에 동시에 추가해야 이 테스트가 계속 green이다.


9. 테스트 위치

파일대상
src/composables/__tests__/useMeterReadings.spec.js신규 — 시드 초기화·usageOf·setReading·파생 동등성·전파·importCsv·전기 과세
src/composables/__tests__/useAllocations.spec.jsbasisQuantity 재배선 회귀0 (기존)
src/composables/__tests__/useMetering.spec.jsmeteringFor 다utility (기존 + 전기 케이스)
src/composables/__tests__/MeteringBlocks.spec.js고지서 검침 블록 표시 (기존)

10. 확장 포인트

가스·난방 등 추가 유틸리티

부과항목 편집기(설정 → 항목)에서 활성 chargeBasis: '사용량' 항목을 추가하면:

  • 사용량 입력 화면의 부과 항목 Select에 자동으로 추가.
  • importCsv가 해당 항목 CSV 행을 자동 인식.
  • useMetering.meteringFor가 자동으로 루프에 포함.
  • 코드 변경 불필요.

누진요율

현재 pool × lineWeight 비례 배분 구조. 누진요율 도입 시 basisQuantity 계층에서 누진 변환 함수를 추가하고 pool 배분 로직을 조정해야 한다.

importCsv 버전 카운터

현재 watch(readings, initDraft, {deep}) 방식. 성능 이슈 시 importVersion 카운터 ref를 useMeterReadings에 추가하고 watch 대상을 변경할 수 있다.


11. 데이터 흐름 요약

demoSeed.meterReadings (2층 시드)
  → useMeterReadings.readings (deep-clone, reactive 싱글톤)
    ↓ setReading / importCsv
  → useMeterReadings.usageOf (파생)
    → useAllocations.basisQuantity '사용량' (재배선)
      → lineWeight = usageOf × dayRatio
        → pool 배분 → 세대별 부과금액
          → 청구·수납·고지서·연령분석·분개
  → useMetering.meteringFor (고지서 검침 블록 reactive)
MeteringGridOrg (draft 편집 → setReading → watch → initDraft)
SheetImportMeteringArt (importCsv → readings 변경 → watch → initDraft)