다크모드
관리비 기본설정 저장 + 콘솔 개요 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/general | src/views/service-charge/setting/general/IndexView.vue | 기본설정(부가세 산출·끝수처리·중간정산) |
/service-charge/actual/console/general | src/views/service-charge/actual/console/general/IndexView.vue | 실 청구(actual) 콘솔 개요 |
/service-charge/provisional | src/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 불변). settingsref:{ vatCalcBasis, roundingSupply:{unit,mode}, roundingVat:{unit,mode}, interimSettlementMonths }.- 개별 getter(
vatCalcBasis/roundingSupply/roundingVat/interimSettlementMonthscomputed) — 소비처가 전체 대신 필요한 값만 구독 가능. 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.generalSettings—watch(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-md2열(라벨/값), EDIT는 전 필드를 펼친 2열 field grid. 헤더 제목과 중복되는 고지서명·연/월은 READ 표에서 제거한다. - 편집 시트: 유형 B(단일 콘텐츠) 패턴 유지 — disclosure 래퍼 없음,
sheet-body에field폼 필드 직접 배치.sheet-footer에 취소/확인. - 읽기 카드(MainOrg): 유형 A 다중 카드 시트가 아니라 페이지 단위 세로 나열(CLAUDE.md §0-4)에 해당 —
disclosure disclosure-md bg-neutral-minimal disclosure-divide-y3개, 각disclosure-footer에 정보 배지 + [편집] 버튼(별도 시트를 여는command="show-modal" commandfor="..."— 카드 내 인라인 편집 아님).
5. 테스트 위치
| 파일 | 대상 |
|---|---|
src/composables/__tests__/useBillingGeneralSettings.spec.js | setter 반영·localStorage 왕복(seed→setter→persist→reload)·DEFAULT 불변(clone-replace) |
src/components/service-charge/setting/general/__tests__/GeneralSettings.spec.js | 3개 시트 draft/dirty/commit 배선 + MainOrg read 바인딩(반응성 포함). dirty=false 가드 테스트는 참조동일성(toBe)으로 setter 미호출을 실판별(설정 SO-4a — 값 비교 tautology 제거) |
src/composables/__tests__/useProvisionalSettlement.spec.js | explicit-input 잠정정산 계산과 검증 |
src/composables/__tests__/vatRoundingCrossConsistency.spec.js | 4개 VAT 소비처(청구·세금계산서·GL·부과항목명세) default 동치 + 설정 변경 시 일관 반영 크로스체크 |
src/composables/__tests__/useBillingOverview.spec.js | overviewKpis 독립 크로스체크(agingSummary와 일치)·entityCounts 실값 |
src/components/service-charge/actual/__tests__/HeaderOrg.spec.js | entityCounts 카운트 표시 |
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 전환 시 useBillingGeneralSettings의 loadInitial/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개)