Skip to content

BE 참고 — 청구 상세 시트 / 뷰모델 계약 · 성격×세금 매핑 · 분개 불변식

독자: BE 개발자/AI. 청구 상세 시트(SheetInvoicingDetailArt, 진입 = 부과 콘솔 3축 행 클릭 → 공유 ref selectedBilling{axis,key})의 뷰모델 계약·집계 분개 의미론·성격×세금 구성요소 매핑·공급가액/공급대가 정의·E2 분개 불변식(3축 공통)·이월 한계·VAT 단일 스케줄 가정을 정의한다. 정본 연계: 성격 레지스트리 src/composables/billingNature.js, 청구 분개 docs/handoff/backend/journal-by-nature.md, 성격 설계 spec docs/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 정의

섹션 keyrowstotal
과세세전 부과원금=과세원금, VAT=vat, 부과원금=과세원금+vat과세원금 + vat
면세세전 부과원금=면세원금, 부과원금=면세원금면세원금
영세세전 부과원금=영세원금, VAT=0, 부과원금=영세원금영세원금
부채장기수선충당금=liability, 부과원금=liabilityliability
자본예비비적립금=capital, 부과원금=capitalcapital
  • 항상 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, 공동주택 프리셋)

  1. DR 미수관리비(0108) = total (공급대가)
  2. CR 용역매출(0412) = 과세원금 + 면세원금 + 영세원금 (매출 합산 1행)
  3. CR 장기수선충당부채(9112) = 장기수선충당금 (프리셋 공급 계정)
  4. CR 예비비적립금(9201) = 예비비적립금 (프리셋 공급 계정)
  5. CR 부가세예수금(0255) = vat (과세원금>0일 때만 존재)

불변식 (contract 축 단일 전표 기준)

불변식수식
차대균형Σ(차변) == Σ(대변)debitSum == creditSum
미수=공급대가debitSum == current.total (journal cross-check)
섹션Σ=공급대가Σ(section.total) == current.total
supplyTotal+VAT=totalcurrent.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=totalcurrent.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' (데모 현재 청구기). vatRateAtPRESET_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()ccslines.unitCode로 열거하나 supplyTotalinvoicingByUnit().currentPeriod에서 오므로, 한 계약이 여러 유닛에 걸치면(현 데이터 모델 범위 밖 — contractAtoms/unitCodesByContract는 다유닛 계약을 표현 가능) 그 계약 전표 total이 타 유닛분까지 포함해 vat = debitSum − supplyTotal 역산이 cross-unit 금액을 VAT로 흡수한다. 멀티유닛 계약을 도입하려면 유닛 축 집계를 라인 단위로 재설계해야 한다(현재는 계약 단위 전표 합산). 멤버 축은 계약→멤버가 N:1이라 이 제약과 무관.