다크모드
사용량 입력 — FE 메인테이너 참고
관리비 상용화 프로그램 #4. 설계 정본:
docs/superpowers/specs/2026-07-01-metering-input-canon-design.md.
1. 라우트
| 경로 | 뷰 | 설명 |
|---|---|---|
/service-charge/metering/general | src/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동형). readingsref:readings[chargeCode][unitCode] = { twoMonthsAgo, prevMonth, currentMonth }.- 모듈 로드 시
buildSeed(DEMO_CONFIG).meterReadingsdeep-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 } }MainOrg가selectedChargeCode를 소유하고 활성 사용량 항목의 첫 항목을 기본 선택한다.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.vue가 provide('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.json — billing.serviceCharge.meteringPage.*:
| 키 | 값 예시 |
|---|---|
title | 사용량 입력 |
chargeItem | 부과 항목 |
columns.unit | 호 |
columns.usage | 당월 사용량 |
import.template | 템플릿 다운로드 |
import.open | 가져오기 |
import.paste | CSV 파일 선택 |
import.run | 가져오기 실행 |
import.result | N건 추가, M건 실패 (메시지) |
en.json 패리티(2026-07-10): 위 meteringPage.* 11키를 포함해 en.json에 실측 97개 leaf 키가 누락돼 있었다(감사 추정 "113"은 상향 조정 전 근사치 — 실제 leaf-diff 계측이 정본). 전량 보충 완료(11ef3cd9b). 회귀 가드: src/i18n/__tests__/localeParity.spec.js — ko.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.js | basisQuantity 재배선 회귀0 (기존) |
src/composables/__tests__/useMetering.spec.js | meteringFor 다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)