Skip to content

FE 메인테이너 — 청구 상세 시트

독자: FE 메인테이너/AI. "이 시트를 이어 개발하려면 무엇을 알아야 하나"를 정의한다. 정본 spec: docs/superpowers/specs/2026-06-25-invoicing-detail-sheet-functionalization-design.md. BE 계약: docs/handoff/backend/invoicing-detail-sheet.md.


1. 진입 (라우트 없음 — 시트 오버레이)

  • 전용 페이지/라우트는 없다. 상세는 시트 오버레이(SheetInvoicingDetailArt, dialog id sheet-invoicing-detail-art)로 연다.

  • 3축 모두 배선 완료 — 각 축의 DynamicTableOrg(invoicing 하위)에 openInvoicingDetail(invoicing)이 정의돼 있고, canon(service-charge actual) 행의 총청구금 버튼 클릭 시 selectedBilling 세팅 + 시트 열기가 동시에 일어난다.

    파일selectedBilling 세팅
    contractinvoicing/contract/blocks/DynamicTableOrg.vue{ ...a, axis: 'contract', key: a.contractCode }
    unitinvoicing/unit/blocks/DynamicTableOrg.vue{ ...invoicing, axis: 'unit', key: invoicing.id } (id = unitCode)
    memberinvoicing/member/blocks/DynamicTableOrg.vue{ ...invoicing, axis: 'member', key: invoicing.id } (id = memberCode)
  • 인라인 펼침(멤버/유닛 브레이크다운)은 행 셰브론(›)toggleExpand(invoicing.id). 총청구금 클릭과 역할이 분리돼 있다(3차 T3 해소).

  • 공유 ref: src/components/billing/_core/console/overlays/selectedBilling.js.


2. 컴포넌트 트리

SheetInvoicingDetailArt.vue                 (시트 루트, dialog id "sheet-invoicing-detail-art")
├─ sheet-header: "청구상세" + close 버튼
├─ zone-top: 계약명+기간배지 / 관리코드 우상단       (vm.header)
├─ zone-middle (v-if vm.current):
│   ├─ 당기분 부과금 card   (disclosure-md)         (vm.current)
│   │   └─ sections v-for: 과세/면세/영세/부채/자본
│   │       └─ rows table-static
│   ├─ 이월 미납분 card    (disclosure-md)         (vm.forward)
│   │   └─ sections v-for (이월 outstanding 세구분) / 없으면 note만
│   ├─ 합계 부과금 card     (disclosure-md)         (vm.grandTotal)
│   │   └─ sections v-for (= 당기 + 이월 병합)
│   └─ 분개 미리보기 card   (disclosure-md, v-if vm.journal)
│       └─ table-static: 계정 / 차변 / 대변 + tfoot 합계
└─ sheet-footer: (빈 푸터 — 현재 액션 없음)

파일 위치: src/components/billing/_core/console/invoicing/overlays/SheetInvoicingDetailArt.vue.


3. 소비 컴포저블 · 데이터 흐름

컴포저블/모듈역할
selectedBilling진입점→시트 선택 공유 ref {axis, key, contractCode, unitName, memberName, start, end}
useInvoicingDetaildetailFor(selection) — axis-aware 배분·분개 조립 뷰모델 파생; 내부 resolve() + combineJournal()
useAllocationsinvoicingByContract/Unit/Member() (3축 집계), lines, contracts, formatAmount
useBillingJournalvouchersInvoicing() — E2 청구확정 전표 목록
billingNature성격 레지스트리 모듈 — withVat(byComp, date) named export로 공급가액→공급대가 VAT 파생(분개 없을 때 폴백)

흐름

DynamicTableOrg (contract / unit / member 축)
  openInvoicingDetail / openAtomDetail
    → selectedBilling.value = { axis: 'contract'|'unit'|'member', key, ... }
      + command="show-modal" commandfor="sheet-invoicing-detail-art"

SheetInvoicingDetailArt
  vm = computed(() => detailFor(selectedBilling.value))
  cards = computed(() => [당기, 이월, 합계])   ← vm.current 존재 시만

useInvoicingDetail.detailFor(selection)
  1. resolve(selection) → { byComp, supplyTotal, ccs, axisTitle }
       axis='contract': row = invoicingByContract().find(cc === key), ccs=[key]
       axis='unit':     row = invoicingByUnit().find(unitCode === key),
                        ccs = lines.filter(unitCode===key).map(contractCode)  (다계약)
       axis='member':   row = invoicingByMember().find(memberCode === key),
                        ccs = contracts.filter(memberCode===key).map(contractCode) (다계약)
  2. journal = combineJournal(ccs)
       → 구성 전표(bj-inv-*)를 side|accountCode별 합산 → { lines, debitSum, creditSum }
       → N=1이면 단일 전표와 동등. 전표 없으면 null.
  3. vat = journal != null
         ? journal.debitSum - supplyTotal   (역산 — 다계약 반올림 드리프트 제거)
         : withVat(byComp, REFERENCE_DATE).부가세 (폴백)
  4. sections = buildSections(byComp, vat)   ← 5섹션 [과세/면세/영세/부채/자본]
  5. forward = forwardingByCompFor(axis, key) 이월 차수 byComponent 합산
       → fwdVat = fwdByComp.부가세, fwdSupply = Σ(나머지 키)
       → forward.sections = buildSections(fwdByComp, fwdVat) (없으면 total 0 + note)
  6. grandTotal = 당기 + 이월 (원금 키 합산 + VAT 별도 합산 → buildSections)
  → { header, current:{supplyTotal,vat,total,sections},
      forward:{supplyTotal,vat,total,sections,note}, grandTotal:{supplyTotal,vat,total,sections},
      journal:{lines,debitSum,creditSum}|null }
  • selection 또는 key가 없거나 해당 행을 못 찾으면 → EMPTY(전 필드 null) 반환, throw 없음.
  • vm.current null이면 시트 body에 "청구 대상을 선택하면 상세가 표시돼요" 안내만 표시.

4. DS 패턴 (CLAUDE.md 준수)

  • 시트 = dialog ... sheet sheet-right sheet-lg sheet-width-3xl sheet-inset-edged sheet-filled sheet-divide-y. sheet-width-3xl(48rem) — 세로 폼/상세 집중 폭 기준.
  • 시트 body = bg-neutral-subtle. 카드 래퍼 = bg-neutral-minimal + disclosure disclosure-md disclosure-inset-edged disclosure-divide-y.
  • summary zone-right = Σ(공급대가)formatAmount(card.data.total) / formatAmount(section.total). 접힌 상태에서도 합계 정보 손실 없음(카드 규율2).
  • 섹션 내 행 테이블 = table table-sm table-static table-divide-y w-full.
  • 분개 카드도 동일 disclosure-md 패턴, tbody 행 + tfoot 합계행.
  • 이월 미납분 카드는 이월 outstanding 세구분 sections를 렌더(당기 카드와 동형). 이월 없으면 total 0 + 안내 note(body-xs).
  • 현재 sheet-footer 액션 없음 — 청구 확정·전송 액션은 이 시트 범위 밖(후속).

5. 게이팅

  • useModuleSubscription(accountingEnabled) 게이팅 없음 — 분개 미리보기 카드는 vm.journal(null이면 숨김)으로만 제어된다.
  • vm.journal이 null인 경우: 해당 contract의 'bj-inv-'+contractCode 전표를 vouchersInvoicing()에서 찾지 못한 경우(데이터 갭 또는 non-canon). 현재 서비스차지 actual 에디션의 seed 계약에는 전표가 존재하므로 정상 시나리오에서는 journal 카드가 항상 표시된다.

6. 선택 ref 구조

js
// contract 축 (openAtomDetail(a)가 세팅)
{
  axis: 'contract',
  key: a.contractCode,     // detailFor → invoicingByContract().find(cc===key)
  contractCode: a.contractCode,
  unitName: a.unitName,
  memberName: a.memberName,
  start: a.start,          // periodLabel 표시용
  end: a.end
}

// unit 축 (openInvoicingDetail(invoicing), invoicing.id = unitCode)
{
  axis: 'unit',
  key: invoicing.id,       // detailFor → invoicingByUnit().find(unitCode===key)
  ...invoicing             // unitDisplayName, billingPeriod 등 테이블 행 필드 포함
}

// member 축 (openInvoicingDetail(invoicing), invoicing.id = memberCode)
{
  axis: 'member',
  key: invoicing.id,       // detailFor → invoicingByMember().find(memberCode===key)
  ...invoicing
}

header.title = [unitName, memberName].filter(Boolean).join(' ') or axisTitle(unit.name / member.name) 폴백. header.periodLabel = start/end 형태(start·end 둘 다 있을 때만). header.accountCode = selection.contractCode ?? key (unit/member 축에선 key = unitCode/memberCode).


7. 확장 포인트

  • 이월분 표시 (✅ 2026-06-27 구현): vm.forwarduseForwarding의 이월 차수 byComponent(충당 후 잔여 outstanding) 합산으로 세구분 분해된다(buildSections 재사용). grandTotal = current + forward. ⚠ roll-forward(월말 미수→차월 이월 자동승격) 런타임 엔진은 미구현 — 정적 데모는 시드 priorPeriods 고정. 실 BE에서 월 경계 모델 필요(forward 표시 자체는 무관·완성).
  • 청구 확정·전송 액션: 현재 sheet-footer가 비어 있다. "전송" 버튼·"청구 확정" 버튼을 footer에 추가 시 postingStatus·sendStatus 상태 연동이 필요하다.
  • 세금계산서 발행 연계: vm.header.accountCode(contractCode or unitCode)로 tax-documents 모듈과 연결해 해당 계약/유닛의 계산서 발행 이력을 탭으로 추가 가능.
  • 딥링크: 현재 URL 불변. 공유가 필요하면 selectedBilling을 쿼리(?detail=u-101)와 동기화하는 라우트 가드 추가(현재 범위 밖).

8. 테스트 위치

대상테스트
뷰모델 불변식 — 3축 전체 (공급대가·섹션Σ·분개 cross-check·이월·균형·null-safe·5섹션)src/composables/__tests__/useInvoicingDetail.spec.js
이월 표시 배선 + E1 개시분개 불변식 (a~h: 섹션Σ·세구분합·E1 대차·net+vat·grand=당기+이월·3축·재무상태표한정·회귀)src/composables/__tests__/forwardingCarryoverDisplay.spec.js
유닛/멤버 집계 불변식 — Σ(sections[].total) === current.total === journal.debitSum === creditSum (N>1 포함)同上
N=1 퇴화 — 유닛 단일계약 = contract 축과 수치 동일 검증同上
분개 전표 소스src/composables/__tests__/useBillingJournal.spec.js
byComponent 원천src/composables/__tests__/useAllocations.spec.js
VAT 파생src/composables/__tests__/billingNature.spec.js
총청구금 클릭 → sheet 진입 배선(unit/member)src/components/billing/_core/console/invoicing/unit/blocks/__tests__/InvoicingTotal.wire.spec.js
총청구금 클릭 → sheet 진입 배선(member)src/components/billing/_core/console/invoicing/member/blocks/__tests__/InvoicingTotal.wire.spec.js

실행: npx vitest run. Playwright 시나리오(수동): 부과 콘솔 → 유닛/멤버/계약 탭 → canon 행 총청구금 클릭 → 시트 오버레이(URL 불변) · 5섹션 + 분개 카드 표시(다계약 합산) · 접기/펼치기 Σ 일치 · 시트 닫기 → 목록 유지, console 0.

2026-06-26 — 유닛/멤버 총청구금 집계 상세 시트 배선 (3차 결함 T3 최종 해소)

  • 3차 T3 워크어라운드: 총청구금 클릭 = toggleExpand(인라인 펼침)로 임시 처리. sheet는 열려도 unit/member key 미전달 → 빈 상태.
  • 최종 해소: useInvoicingDetail.detailForaxis 분기(unit/member) 추가 + combineJournal(ccs) 다계약 합산 → unit/member 집계 뷰모델 완전 구현. DynamicTableOrg(unit·member) 총청구금 클릭 = openInvoicingDetail(invoicing)selectedBilling = { axis, key } + commandfor="sheet-invoicing-detail-art". 인라인 펼침은 행 셰브론(›)(toggleExpand)으로 이동(역할 분리).
  • VAT 역산: N>1 집계 시 journal.debitSum - supplyTotal (per-contract 반올림 드리프트 제거). N=1이면 withVat() 결과와 수치 동일.
  • 불변식(3축 공통): Σ(sections[].total) === current.total === journal.debitSum === journal.creditSum. useInvoicingDetail.spec.js에서 매 배포마다 검증.
  • 테스트: useInvoicingDetail.spec.js(불변식·N=1·EMPTY) + InvoicingTotal.wire.spec.js(unit/member 배선).