Skip to content

관리비 기본설정 저장 + 콘솔 개요 KPI — FE 메인테이너 참고

관리비 상용화 잔여 프로그램(설정 SO). 설계 정본: docs/superpowers/specs/2026-07-08-billing-settings-overview-design.md. 2026-07-26 개요 화면 갱신: 펼침형 헤더 stage 4카드를 제거하고 본문 현재 단계 요약으로 대체했다. KPI는 같은 정산회차 기준의 부과액·수납액·미수잔액·수납률 4개로 축소하고, 정산 정보는 기본 접힘·2열 read table로 정리했다.


1. 라우트

경로설명
/service-charge/setting/generalsrc/views/service-charge/setting/general/IndexView.vue기본설정(부가세 산출·끝수처리·중간정산)
/service-charge/actual/console/generalsrc/views/service-charge/actual/console/general/IndexView.vue실 청구(actual) 콘솔 개요
/service-charge/provisionalsrc/views/service-charge/provisional/IndexView.vue독립 중간정산 목록·생성·확정

2. 컴포넌트 트리

2-1. 기본설정 (일반 탭)

src/components/service-charge/setting/general/
├── MainOrg.vue                              — 3개 disclosure 카드(read binding, useBillingGeneralSettings 소비)
└── overlays/
    ├── SheetUpdateAccountInfoArt.vue         — 부가세 산출기준 편집 시트
    ├── SheetUpdateRoundingArt.vue            — 끝수처리(공급가/VAT) 편집 시트
    └── SheetUpdateInterimSettlementArt.vue   — 중간정산 개월수 편집 시트

2-2. 콘솔 개요 (actual 전용)

src/components/service-charge/actual/
├── HeaderOrg.vue                            — 세대/세대원/사업자/차량 카운트 (entityCounts)
└── console/general/
    ├── HeaderOrg.vue                        — 정적 제목 + ConsoleTabBarOrg(개요)
    ├── MainOrg.vue                          — 현재 단계 요약 + KPI 4개 + 정산 정보 + 차수 이력
    └── blocks/DynamicTableOrg.vue           — 기본 접힘 정산 정보(read↔inline edit)

useBillingOverview가 카운트·KPI 소스를 제공한다. 중간정산은 월별 부과·수납 KPI 콘솔을 소비하지 않고 전용 RPC 목록을 사용하는 독립 제품 화면이다.


3. 소비 컴포저블

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

src/composables/useBillingGeneralSettings.js

  • 모듈 레벨 reactive 싱글톤(useCharges·lateFeePolicy 동형 — clone-replace, DEFAULT 불변).
  • settings ref: { vatCalcBasis, roundingSupply:{unit,mode}, roundingVat:{unit,mode}, interimSettlementMonths }.
  • 개별 getter(vatCalcBasis/roundingSupply/roundingVat/interimSettlementMonths computed) — 소비처가 전체 대신 필요한 값만 구독 가능.
  • setVatCalcBasis/setRounding({scope,unit,mode})/setInterimSettlementMonths(n) — 각각 유효성 검사 후 clone-replace, 실패 시 {ok:false, reason} 반환(변이 없음).
  • applyRounding(value, {unit,mode}) — named export, Vue 무관 순수함수. VAT 계산 소비처(billingNature·useAllocations·useBillingJournal·useInvoicingDetail)가 직접 import한다.
  • localStorage 키 leyve.billing.generalSettingswatch(settings, ..., {deep:true})로 자동 영속.

3-2. MainOrg.vue — read 바인딩 패턴

src/components/service-charge/setting/general/MainOrg.vue

js
const { settings } = useBillingGeneralSettings();
const vatCalcBasisLabel = computed(() =>
  settings.value.vatCalcBasis === "항목별" ? "각 항목별 산출" : "공급가 합산 후 산출",
);
const unitLabel = (unit) =>
  unit === 1
    ? "0.1의 자리(1원 미만)"
    : unit === 10
      ? "1의 자리(10원 미만)"
      : "10의 자리(100원 미만)";
const modeLabel = (mode) =>
  mode === "반올림" ? "반올림(사사오입)" : mode === "올림" ? "올림(절상)" : "내림(절사)";

surcharge MainOrg 라벨 패턴과 동일 — canon 값(한글 enum) → 화면 표시 문구 매핑을 computed로 격리해 하드코딩 문자열을 제거했다.

라벨 통일 주의(설정 SO-4a): modeLabel의 내림 라벨('내림(절사)')이 canon 어휘 정본이다. SheetUpdateRoundingArt.vue의 Select 옵션 라벨도 동일하게 '내림(절사)'로 맞춰져 있다(이전에 '버림(절사)'로 동의어가 어긋나 있었으나 통일함). 새 화면에서 끝수처리 방식을 표시할 때는 반드시 이 어휘(반올림(사사오입)/올림(절상)/내림(절사))를 그대로 사용한다.

3-3. 3개 시트 — mode 무관 단순 draft→commit 패턴

세 시트(SheetUpdateAccountInfoArt/SheetUpdateRoundingArt/SheetUpdateInterimSettlementArt) 모두 다음 공통 구조를 쓴다(CLAUDE.md §0 read↔edit 전체시트 대상은 아니고, 유형 B 단일 콘텐츠 편집 시트 — 이미 별도 시트로 분리되어 있어 그 관용을 유지):

js
const draft = ref(toOption(settings.value.현재값)); // Select value ↔ canon enum 매핑
const dirty = computed(() => fromOption(draft.value) !== settings.value.현재값);
watch(
  () => settings.value.현재값,
  (v) => {
    if (!dirty.value) draft.value = toOption(v);
  },
); // 외부 변경 재동기화
const resetDraft = () => {
  draft.value = toOption(settings.value.현재값);
}; // 취소/닫기 시
const commit = () => {
  if (!dirty.value) return; // 가드 — dirty 아니면 setter 미호출
  setXxx(fromOption(draft.value));
};
defineExpose({ draft, dirty, commit, resetDraft });
  • Select value ↔ canon 매핑: DS Select 컴포넌트는 문자열 value만 받으므로(option1.. 또는 round/ceil/floor), canon 한글 enum(반올림/올림/내림, 1/10/100)과 양방향 변환 헬퍼(toOption/fromOption, MODE_TO_CANON/CANON_TO_MODE)를 각 시트 script에 둔다.
  • dirty 게이트: SheetUpdateSurchargeRate 선례와 동일 — [확인] 버튼은 :disabled="!dirty". 값이 실제로 바뀌었을 때만 활성화.
  • SheetUpdateRoundingArt는 2개 scope(supply/vat)를 한 시트에서 관리commit()setRounding({scope:'supply',...})setRounding({scope:'vat',...})를 순차 호출한다. dirty는 두 scope 중 하나라도 바뀌면 true.
  • 닫기 버튼(X)도 resetDraft를 호출 — 저장하지 않고 닫으면 draft가 버려지고 다음에 열 때 현재 settings로 재시드.

3-4. useAllocations — VAT 라운딩 재배선

src/composables/useAllocations.js

js
import { useBillingGeneralSettings, applyRounding } from "./useBillingGeneralSettings";
const { roundingVat } = useBillingGeneralSettings();
// pushDocs() 내부:
const vat = cat === "과세" ? applyRounding(supply * vatRateAt(), roundingVat.value) : 0;

세금계산서 생성 시 과세 라인의 세액 계산이 Math.round 직접 호출에서 applyRounding(..., roundingVat.value)로 교체되었다. default 값에서 완전 동치(§4 BE 문서 참고).

3-5. billingNature.withVat — 청구 VAT 파생 재배선

src/composables/billingNature.js

js
const { roundingVat } = useBillingGeneralSettings();
export function withVat(byComponent, date = REFERENCE_DATE) {
  const out = { ...(byComponent || {}) };
  const supply = out.과세원금 || 0;
  if (supply > 0) out.부가세 = applyRounding(supply * vatRateAt(date), roundingVat.value);
  return out;
}

withVat는 청구 표시(useInvoice 등)·useBillingOverview.totalBilled()가 공통으로 소비하는 단일 VAT 파생 지점이다.

3-6. useBillingJournal — GL 전표 VAT 재배선

src/composables/useBillingJournal.js vouchersInvoicing()/vouchersInvoicingByCharge() — 동일하게 applyRounding(..., roundingVat.value) 적용. GL 대사 vitest는 default 값에서 무변.

3-7. useInvoicingDetail — 부과항목 명세 VAT 재배선 + plug 라인

src/composables/useInvoicingDetail.js itemsFor() — 라인별 applyRounding 적용 후, 반올림 비가산성으로 라인합계와 시트 전체 authoritative VAT가 어긋나면 마지막 과세 라인이 diff를 흡수(plug line). 청구 상세 시트(InvoicingDetailSheet)의 "부과 항목 명세" 탭에서 Σ(라인 VAT) === 전체 VAT 불변식을 유지하기 위함.

3-8. 중간정산 create draft — lookbackMonths 초기값

SheetCreateProvisionalSettlementArt.vue

js
const { interimSettlementMonths } = useBillingGeneralSettings();
const newDraft = () =>
  makeProvisionalSettlementDraft(null, {
    defaultLookbackMonths: interimSettlementMonths.value,
  });

전역 settlementParams와 override watch는 없다. 설정값은 새 create draft의 초기값일 뿐이고, 생성 후 row의 값은 중간정산 서버 repo가 독립 보존한다. useProvisionalSettlement는 explicit input pure calculator다.

3-9. useBillingOverview (신규 — 집계자, canon 아님)

src/composables/useBillingOverview.js

js
export function useBillingOverview() {
  const { units, members, invoicingByUnit } = useAllocations();
  const { receipts } = useReceipts();
  const { agingSummary } = useReceivableAging();
  // ...
  return { overviewKpis, entityCounts };
}
  • 기존 컴포저블 3개(useAllocations/useReceipts/useReceivableAging) + billingNature.withVat를 조합만 한다. 자체 상태(ref)를 두지 않는 순수 조합자 — 매 호출마다 최신값 재계산.
  • overviewKpis(axis='unit'){ 부과총액, 수납액, 수납률, 미수금, 연체건수, 최장연체차수 }.
  • entityCounts(){ 세대수, 세대원, 사업자, 차량 }.

3-10. HeaderOrg.vue — entityCounts 배선

src/components/service-charge/actual/HeaderOrg.vue

js
const { entityCounts } = useBillingOverview();
const counts = computed(() => entityCounts());

템플릿의 {{ counts.세대수 }} 등이 이전 하드코딩 999를 대체. computed로 감싸 units/members reactive 변경에 반응(향후 마스터데이터 편집 배선 대비 — 현재는 정적 시드라 실질적으로 재계산 트리거 없음).

펀치리스트 I-6 확장(2026-07-10): 동일한 3줄 배선(import+computed(entityCounts)+템플릿 4값)을 service-charge/setting/{general,surcharge,collecting,item/general,item/group}/HeaderOrg.vue 5개 파일에도 그대로 복제했다(78bcd15cf). 콘솔 개요 헤더와 설정 화면 헤더가 이제 같은 실 카운트 소스(useBillingOverview().entityCounts())를 공유 — 새 설정 하위화면을 추가할 때도 이 3줄 패턴을 그대로 복사하면 된다.

3-11. console/general/MainOrg.vue — 현재 단계와 KPI 카드 배선

src/components/service-charge/actual/console/general/MainOrg.vue

js
const { overviewKpis } = useBillingOverview();
const kpis = computed(() => overviewKpis());
const fmt = (n) => Math.round(n ?? 0).toLocaleString("ko-KR");
const fmtPercent = (r) => `${((r ?? 0) * 100).toFixed(1)}%`;

useConsoleStage()에서 첫 미확정 stage를 찾아 현재 단계와 상태만 한 줄로 요약한다. 업무 진입은 상단 ConsoleTabBarOrg가 단독 소유한다. 개요 카드에 같은 목적지의 CTA를 중복 배치하지 않는다.

KPI는 grid grid-cols-2 lg:grid-cols-4 gap-4에 다음 네 값만 같은 위계로 배치한다.

화면 라벨overviewKpis()기준
부과액부과총액당기 발생
수납액수납액 = agingSummary.총회수당기 수납 + 당기 선수금 충당
미수잔액미수금 = agingSummary.총차기이월당기말 잔액
수납률총회수 / 총당기발생같은 정산회차

연체건수·최장연체차수는 overviewKpis() 데이터 계약에는 남지만 개요 첫 화면에서는 제거했다. 미수금 연령분석 화면이 상세를 소유한다. 부과액이 0이어도 수납액·미수잔액을 로 숨기지 않으며, 분모가 없는 수납률만 로 표시한다.


4. DS 패턴 메모

  • 현재 단계: card-sm 한 줄 표면에서 라벨과 단계·상태 배지만 제공한다. 내비게이션은 상단 탭에 맡기고, 헤더 disclosure의 4×stage 카드와 상태 중복을 만들지 않는다.
  • KPI 카드: card card-sm card-inset-edged bg-neutral-minimal + card-body flex-col gap-1 + subtitle-sm text-dimmed + title-lg + body-sm text-dimmed. 모바일 2×2, 데스크톱 4열.
  • 정산 정보: READ는 기본 접힘 disclosure + table-md 2열(라벨/값), EDIT는 전 필드를 펼친 2열 field grid. 헤더 제목과 중복되는 고지서명·연/월은 READ 표에서 제거한다.
  • 편집 시트: 유형 B(단일 콘텐츠) 패턴 유지 — disclosure 래퍼 없음, sheet-bodyfield 폼 필드 직접 배치. sheet-footer에 취소/확인.
  • 읽기 카드(MainOrg): 유형 A 다중 카드 시트가 아니라 페이지 단위 세로 나열(CLAUDE.md §0-4)에 해당 — disclosure disclosure-md bg-neutral-minimal disclosure-divide-y 3개, 각 disclosure-footer에 정보 배지 + [편집] 버튼(별도 시트를 여는 command="show-modal" commandfor="..." — 카드 내 인라인 편집 아님).

5. 테스트 위치

파일대상
src/composables/__tests__/useBillingGeneralSettings.spec.jssetter 반영·localStorage 왕복(seed→setter→persist→reload)·DEFAULT 불변(clone-replace)
src/components/service-charge/setting/general/__tests__/GeneralSettings.spec.js3개 시트 draft/dirty/commit 배선 + MainOrg read 바인딩(반응성 포함). dirty=false 가드 테스트는 참조동일성(toBe)으로 setter 미호출을 실판별(설정 SO-4a — 값 비교 tautology 제거)
src/composables/__tests__/useProvisionalSettlement.spec.jsexplicit-input 잠정정산 계산과 검증
src/composables/__tests__/vatRoundingCrossConsistency.spec.js4개 VAT 소비처(청구·세금계산서·GL·부과항목명세) default 동치 + 설정 변경 시 일관 반영 크로스체크
src/composables/__tests__/useBillingOverview.spec.jsoverviewKpis 독립 크로스체크(agingSummary와 일치)·entityCounts 실값
src/components/service-charge/actual/__tests__/HeaderOrg.spec.jsentityCounts 카운트 표시

6. 확장 포인트

KpiStripOrg 추출

현재 4개 KPI 카드 마크업은 actual console/general/MainOrg.vue가 소유한다. 다른 제품에 동일한 KPI strip이 필요해져 두 번째 소비처가 생기면 KpiStripOrg.vue 공용 컴포넌트로 승격한다.

차량/사업자 마스터

entityCounts().차량이 0 고정인 이유는 billing 캐논에 대응 소스가 없기 때문이다. master-data/vehicle 모듈(독립 mock)을 billing과 연동하려면 세대(unit)-차량 FK를 추가하고 useBillingOverview.entityCounts()useAllocations().units와 조인하는 로직을 추가해야 한다. 사업자도 현재는 멤버 임시 필드(taxProfiles/businessRegistrationNumber) 기반 추정 집계라, 전용 사업자 마스터가 생기면 그쪽을 단일 소스로 재배선.

BE 영속

localStorage → 서버 API 전환 시 useBillingGeneralSettingsloadInitial/watch(settings, persist) 부분만 API 호출로 교체하면 되도록 설계되어 있다(setter·sanitize·applyRounding은 API 무관 순수 로직). 자세한 계약은 BE 문서 §6 참고.

provisional P2 (예수금 GL·true-up)

이번 스코프에서 제외됨. useProvisionalSettlement는 현재 추산 라인·합계 계산만 제공하고, 실제 GL 전표 생성이나 사후 true-up(실 청구와의 차액 재분개)은 구현되어 있지 않다. 별도 회계 트랙에서 다룰 예정 — 착수 시 useBillingJournal의 기존 GL 패턴(E-시리즈 전표)을 참고.


7. 데이터 흐름 요약

[기본설정 저장]
설정 3시트(draft→commit) → useBillingGeneralSettings.setXxx() → settings.value 교체
  → watch(deep) → localStorage['leyve.billing.generalSettings']
  → MainOrg.vue computed 라벨 즉시 갱신
  → roundingVat → billingNature.withVat / useAllocations(세금계산서) / useBillingJournal(GL) / useInvoicingDetail(명세)
     4개 소비처 동시 반영(reactive .value 구독)
  → interimSettlementMonths → 새 중간정산 create draft.lookbackMonths 초기값

[콘솔 개요]
useAllocations(units/members/invoicingByUnit) ─┐
useReceivableAging(agingSummary)               ├→ useBillingOverview.{overviewKpis, entityCounts}
useConsoleStage(statusOf)                      ┘     ↓
                                          HeaderOrg.vue(세대/세대원/사업자/차량)
                                          console/general/MainOrg.vue(현재 단계 + KPI 카드 4개)