다크모드
BE 참고 — 청구 상세 시트 / 뷰모델 계약 · 성격×세금 매핑 · 분개 불변식
독자: BE 개발자/AI. 청구 상세 시트(
SheetInvoicingDetailArt, 진입 = 부과 콘솔 3축 행 클릭 → 공유 refselectedBilling{axis,key})의 뷰모델 계약·집계 분개 의미론·성격×세금 구성요소 매핑·공급가액/공급대가 정의·E2 분개 불변식(3축 공통)·이월 한계·VAT 단일 스케줄 가정을 정의한다. 정본 연계: 성격 레지스트리src/composables/billingNature.js, 청구 분개docs/handoff/backend/journal-by-nature.md, 성격 설계 specdocs/superpowers/specs/2026-06-20-billing-charge-nature-design.md. 표기: as-built(현재 프로토 동작) vs 확정규칙(BE 구현 시 따를 규칙). 다르면 명시한다.
1. 뷰모델 계약 — detailFor(selection) 반환 형태
useInvoicingDetail().detailFor(selection) 는 다음 형태를 반환한다.
{
header: {
title: string, // unitName + memberName 또는 contract.name 폴백
periodLabel: string|null, // "start/end" 형태, start·end 둘 다 있을 때만
accountCode: string // contractCode (관리코드 노출, 상세 우상단)
} | null,
current: {
supplyTotal: number, // 공급가액 = Σ(성격별 부과원금) — VAT 미포함
vat: number, // 과세분 VAT = 과세원금 × 10%
total: number, // 공급대가 = supplyTotal + vat (= 미수채권 금액)
sections: Section[] // 5섹션 [과세, 면세, 영세, 부채, 자본] (§2)
} | null,
forward: { // ✅ 이월 미납분 = useForwarding 이월 차수 outstanding (§5)
supplyTotal: number, // 이월 공급가액분 (Σ byComponent − 부가세)
vat: number, // 이월 부가세 (Σ byComponent.부가세)
total: number, // = supplyTotal + vat = forwardingRow.forwarding
sections: Section[], // 5섹션 (이월 없으면 [])
note: string | null // 이월 없을 때만 안내, 데이터 있으면 null
} | null,
grandTotal: { // = 당기 + 이월
supplyTotal: number, // current.supplyTotal + forward.supplyTotal
vat: number, // current.vat + forward.vat
total: number, // current.total + forward.total
sections: Section[] // 원금 키 합산 + VAT 별도 합산 → buildSections
} | null,
journal: {
lines: JournalLine[], // E2 전표 라인 (§4)
debitSum: number, // Σ(차변) = current.total (cross-check 불변식)
creditSum: number // Σ(대변) = debitSum (균형 불변식)
} | null
}selection.axis는 'contract'(기본) | 'unit' | 'member'. 유닛/멤버 축은 구성 계약 전표를 combineJournal(ccs)(§4a)로 합산해 단일 집계 분개를 파생한다. N=1이면 단일 계약 전표와 수치 동일.
selection 또는 key가 없거나 해당 행을 찾지 못하면 → 전 필드 null인 EMPTY 객체 반환 (throw 없음).
2. Section 형태 — 성격×세금 5섹션
Section {
key: '과세'|'면세'|'영세'|'부채'|'자본',
label: string, // '과세분'|'면세분'|'영세분'|'부채분'|'자본분'
total: number, // 섹션 공급대가(공급가액+VAT 포함)
rows: SectionRow[]
}
SectionRow { label: string, value: number }섹션별 rows 정의
| 섹션 key | rows | total |
|---|---|---|
| 과세 | 세전 부과원금=과세원금, VAT=vat, 부과원금=과세원금+vat | 과세원금 + vat |
| 면세 | 세전 부과원금=면세원금, 부과원금=면세원금 | 면세원금 |
| 영세 | 세전 부과원금=영세원금, VAT=0, 부과원금=영세원금 | 영세원금 |
| 부채 | 장기수선충당금=liability, 부과원금=liability | liability |
| 자본 | 예비비적립금=capital, 부과원금=capital | capital |
- 항상 5섹션 전부 반환(값 0인 섹션도 포함). 표시 필터링은 UI 레이어 책임.
- 데이터 원천:
row.currentPeriodByComponent(키:과세원금,면세원금,영세원금,장기수선충당금,예비비적립금).
3. 성격×세금 구성요소 매핑
원천 — billingNature.js 레지스트리
js
// 성격(nature) → 분류(group) + 대변 계정
BILLING_NATURE = {
관리비: { group: '매출', creditAccountName: '용역매출' },
수도료: { group: '매출', creditAccountName: '용역매출' },
장기수선충당금: { group: '부채', creditAccountName: '장기수선충당부채' },
예비비적립금: { group: '자본', creditAccountName: '예비비적립금' }
}
// charge → 구성요소 키
componentOf(charge) =
group === '매출' → `${taxCategory}원금` (예: '과세원금', '면세원금', '영세원금')
group ≠ '매출' → charge.nature (예: '장기수선충당금', '예비비적립금')집계 경로 (as-built)
charge[] (씨드/BE 부과라인)
→ componentOf(charge) 로 구성요소 키 분류
→ useAllocations.lineTotalsByComponent() : { [component]: 금액 }
→ invoicingByContract()[i].currentPeriodByComponent
→ useInvoicingDetail.buildSections(byComp, vat)
→ Section[5] (과세/면세/영세/부채/자본)공급가액 vs 공급대가
| 용어 | 정의 | 수식 |
|---|---|---|
| 공급가액(supplyTotal) | VAT 미포함 순부과금 합계 | Σ(과세원금 + 면세원금 + 영세원금 + 장기수선충당금 + 예비비적립금) = currentPeriod |
| 부가세(vat) | 과세원금에만 10% 적용 | Math.round(과세원금 × 0.1) |
| 공급대가(total) | 고객이 실제 납부할 금액 | supplyTotal + vat = 미수채권(DR 0108) 금액 |
확정규칙: 미수채권 = 공급대가. 공급가액만으로 미수를 세우면 VAT가 빠지므로 오류. BE API는 반드시 receivable = supplyTotal + vat를 공급대가로 제공해야 한다.
4. E2 분개 cross-check 불변식
전표 형태 (vouchersInvoicing()[i])
Voucher {
id: 'bj-inv-' + contractCode, // 조회 키
total: number, // = 공급대가(미수채권)
lines: JournalLine[]
}
JournalLine { accountCode, accountName, debit?, credit? }라인 순서 (as-built, 공동주택 프리셋)
- DR 미수관리비(0108) =
total(공급대가) - CR 용역매출(0412) =
과세원금 + 면세원금 + 영세원금(매출 합산 1행) - CR 장기수선충당부채(9112) =
장기수선충당금(프리셋 공급 계정) - CR 예비비적립금(9201) =
예비비적립금(프리셋 공급 계정) - CR 부가세예수금(0255) =
vat(과세원금>0일 때만 존재)
불변식 (contract 축 단일 전표 기준)
| 불변식 | 수식 |
|---|---|
| 차대균형 | Σ(차변) == Σ(대변) 즉 debitSum == creditSum |
| 미수=공급대가 | debitSum == current.total (journal cross-check) |
| 섹션Σ=공급대가 | Σ(section.total) == current.total |
| supplyTotal+VAT=total | current.supplyTotal + current.vat == current.total |
3축 공통 불변식과 유닛/멤버 집계 분개(combineJournal) 의미론은 §4a 참고.
4a. 집계 분개 의미론 — combineJournal(ccs) (유닛/멤버 축)
유닛/멤버 행은 N개 계약에 걸친 집계이므로 단일 bj-inv-{cc} 전표가 없다. combineJournal(ccs)는 구성 계약의 전표들을 계정코드 + 차/대(side) 기준으로 합산해 단일 집계 분개를 만든다.
combineJournal([cc1, cc2, ...]) →
각 bj-inv-{cc} 전표의 lines를 순회
→ side|accountCode 복합키로 debit/credit 누적합산
→ merged lines + debitSum + creditSum- N=1 퇴화: 구성 계약이 1개면 합산 결과 = 그 단일 전표와 수치·계정 동일.
- 계정 순서: 같은 계정이 여러 계약에 걸쳐 나오면 첫 등장 순서가 lines 순서가 된다. 계정명(accountName)은 first-wins.
- 전표 없는 계약:
vouchersInvoicing()에서 해당bj-inv-{cc}를 찾지 못하면 그 계약분은 합산에서 제외된다. 전부 없으면null반환 → 시트의 분개 카드 숨김. - 같은 기간·단일 부가세 스케줄: 동일 청구기의 계약들이므로 VAT 세율이 균일하며 합산이 의미 있다. 기간이 다른 계약을 가로지르는 합산은 이 모델 범위 밖.
VAT 역산 — journal.debitSum - supplyTotal
유닛/멤버 집계 시 VAT = journal.debitSum - supplyTotal 으로 역산한다.
- 이유: 각 계약의
Math.round(과세원금 × 0.1)반올림이 계약별로 발생하므로,Σ(per-contract vat)와round(Σ(과세원금) × 0.1)이 1원 이상 차이날 수 있다. 전표의debitSum(미수채권 합산)은 이미 per-contract 반올림이 적용된 공급대가이므로,debitSum - supplyTotal역산이 분개와 완전히 일치하는 VAT를 준다. - N=1: 단일 계약에서는
withVat()결과와 수치 동일(드리프트 없음). - 분개 없을 때 폴백:
journal == null이면withVat(byComp, REFERENCE_DATE).부가세로 폴백.
불변식 (3축 공통)
| 불변식 | 수식 |
|---|---|
| 차대균형 | journal.debitSum == journal.creditSum |
| 미수=공급대가 | journal.debitSum == current.total |
| 섹션Σ=공급대가 | Σ(section.total) == current.total |
| supplyTotal+VAT=total | current.supplyTotal + current.vat == current.total |
위 4개 불변식은 계약·유닛·멤버 3축 모두에서 성립한다. useInvoicingDetail.spec.js의 unit/member 루프 테스트에서 N>1 집계 케이스 포함 매 배포마다 검증된다.
5. 이월 미납분 + 전기이월 미수 개시분개(E1) — ✅ 2026-06-27
정본 spec:
docs/superpowers/specs/2026-06-27-forwarding-carryover-display-design.md.
5-1. 표시 (forward 섹션)
vm.forward=useForwarding의 이월(forwarded) 차수byComponent(=deriveByComponent(withVat(billedByComponent), paid+adj, 충당순서)= 충당 후 잔여 outstanding, 부가세 키 포함) 합산.fwdVat = Σ byComponent.부가세,fwdSupply = Σ(나머지 키),forward.total = fwdSupply + fwdVat = forwardingRow.forwarding.forward.sections = buildSections(fwdByComp, fwdVat)— 당기와 동일 5섹션.grandTotal = 당기 + 이월.- 불변식:
Σ byComponent(전 키) === outstanding(deriveByComponent: Σout = billed − reduced). VAT 권위 =byComponent.부가세(전표 bj-inv-* 없음, 정수 누적 무드리프트).
5-2. E1 전기이월 미수 개시분개 (vouchersOpeningReceivable)
이월 outstanding>0 전 계약마다 1전표(prior-only 부과종료·일시중단 포함). 과거 청구(E2)를 재현하지 않으므로 이월 미수를 기초 잔액으로 개시:
DR 미수관리비 (0108) forwardingRow.forwarding (공급대가)
CR 이월이익잉여금 (0375) net = Σ byComponent(− 부가세) [net>0일 때]
CR 부가세예수금 (0255) vat = byComponent.부가세 [vat>0일 때]- E2 미러: 당기 청구(DR미수/CR매출0412+CR부가세예수금)와 동형이되, 당기 매출 대신 전기이월 자본(0375 이월이익잉여금) 대변.
- 재무상태표 한정: 자산↑ = 자본↑ + 부채↑. 손익(IS)·결산 income 무영향(당기 매출 아님). → IS·당기순이익 불변.
- 인식기준 무관 방출: 원금은 발생주의 기준(E2가 청구 시 항상 인식). E3(연체료)만 인식기준 게이트, E1은 없음.
- drift 해소: E3(연체료발생, DR 미수)의 기초 원금이 0108에 정당 반영(
AUDIT-...5TH §58의 "이월미수 개시분개 부재" 해소). E1=이월 원금 / E3=이월 연체료 — 분리(이중계상 없음). billingVouchers()순서:vouchersOpeningReceivable()를 vouchersInvoicing 앞(기초 개시).- ⚠ 미구현: roll-forward 런타임(월말 미수→차월 이월 자동승격). 정적 데모는 priorPeriods 고정. 실 BE 월 경계 모델 필요.
- ⚠ 실 BE 주의(VAT 신고이력 정합): E1이
0255 부가세예수금을 개시로 재수립하는 것은 "과거 부가세 미신고" 전제다. 실 운영에서 과거 청구분 부가세가 이미 신고·납부됐다면 0255 재수립은 부채 과대계상이 된다. roll-forward 실엔진 도입 시, 개시 부가세 대변은 신고이력에 따라 (a) 미신고분만 0255 / (b) 신고완료분은 다른 정산계정으로 분기해야 한다. (정적 데모는 개시 시산표 기법으로 격리 — spec OUT 범위.)
6. VAT 단일 스케줄 가정
- VAT 계산:
Math.round(과세원금 × vatRateAt(REFERENCE_DATE)). REFERENCE_DATE = '2025-12-31'(데모 현재 청구기).vatRateAt는PRESET_VAT_SCHEDULE(1977-07-01 도입, 10% 불변)에서 effective-dated 조회.- 현재 시나리오: 단일 버전(10%) → 분개 POSTING_DATE·세율이 동일 →
withVat결과와 분개 미수금액의 cross-check 항상 성립. - BE 확장 시: 세율 개정이 생기면
setVatSchedule으로 글로벌 스케줄에 미래 버전 추가.vatRateAt(거래일)은 거래일 기준 과거 세율을 소급 반환(적용 기간 내 버전 선택). 취득일·거래일 불일치 주의. - 한계: 현재 VAT는 과세원금에만 단일 세율 적용. 복합 세율(예: 임시 인하 세율 구간)은 스케줄에 버전 추가로 대응 가능하나, 과세원금 내부 항목별 세율 분기는 미지원(전체 과세원금 × 단일 세율).
7. 데이터 원천 요약 (BE → FE API 계약)
BE가 실 API를 제공할 때 FE useInvoicingDetail이 기대하는 데이터 원천:
| 데이터 | 원천 composable | 필요한 BE 필드 |
|---|---|---|
| 구성요소별 당기분 (contract 축) | invoicingByContract()[i].currentPeriodByComponent | { 과세원금, 면세원금, 영세원금, 장기수선충당금, 예비비적립금 } |
| 구성요소별 당기분 (unit 축) | invoicingByUnit()[i].currentPeriodByComponent | 同上 (구성 계약분 합산된 byComponent) |
| 구성요소별 당기분 (member 축) | invoicingByMember()[i].currentPeriodByComponent | 同上 (보유 계약분 합산된 byComponent) |
| 공급가액 합계 | invoicingByContract/Unit/Member()[i].currentPeriod | Σ(byComponent) |
| 구성 계약 코드 목록 (unit) | lines.filter(unitCode===key).map(contractCode) | line 레코드에 unitCode + contractCode 필드 필요 |
| 구성 계약 코드 목록 (member) | contracts.filter(memberCode===key).map(contractCode) | contract 레코드에 memberCode 필드 필요 |
| E2 전표 (계약별) | vouchersInvoicing().find(v.id === 'bj-inv-'+cc) | { id:'bj-inv-{contractCode}', total, lines:[{side,accountCode,accountName,debit?,credit?}] } |
| 이월 미납분 (forward 섹션) | useForwarding.forwardingBy{Contract,Unit,Member}() 행의 periods[kind=forwarded].byComponent | 이월 차수별 { outstanding, byComponent } (§5-1) |
| E1 전기이월 개시전표 | vouchersOpeningReceivable() (bj-open-{key}) | 이월 outstanding → DR 0108 / CR 0375+0255 (§5-2) |
확정규칙 1: BE는 currentPeriodByComponent를 성격×세금 구성요소별로 분해해 제공해야 한다. 단일 합산 금액만 내리면 시트의 5섹션 분해가 불가하다.
확정규칙 2: 전표 ID는 'bj-inv-' + contractCode 규약을 유지해야 한다. combineJournal이 이 규약으로 구성 계약 전표를 조회한다. 유닛/멤버 축 집계는 구성 계약별 전표를 각각 조회 후 합산한다.
확정규칙 3 (전제 불변식 — 1계약=1유닛): 유닛 축 집계의 정합성은 각 구성 계약이 정확히 한 유닛에만 귀속됨에 의존한다. resolve()는 ccs를 lines.unitCode로 열거하나 supplyTotal은 invoicingByUnit().currentPeriod에서 오므로, 한 계약이 여러 유닛에 걸치면(현 데이터 모델 범위 밖 — contractAtoms/unitCodesByContract는 다유닛 계약을 표현 가능) 그 계약 전표 total이 타 유닛분까지 포함해 vat = debitSum − supplyTotal 역산이 cross-unit 금액을 VAT로 흡수한다. 멀티유닛 계약을 도입하려면 유닛 축 집계를 라인 단위로 재설계해야 한다(현재는 계약 단위 전표 합산). 멤버 축은 계약→멤버가 N:1이라 이 제약과 무관.