Skip to content

부과내역서 — FE 메인테이너 참고

관리비 상용화 필수 문서 트랙(펀치리스트 점검 ④)의 2호. 설계 정본: docs/superpowers/specs/2026-07-10-charge-statement-collection-status-design.md. 데이터 계약은 docs/handoff/backend/report-charge-statement.md 참고(본 문서는 라우트·컴포넌트·인쇄 뷰 중심). ①총괄표 FE handoff(report-summary-table.md) 패턴을 그대로 복제.


1. 라우트 / feature 게이트

1-1. 콘솔 탭 (billingLines.js)

신규 탭 없음. 보고서 탭(group:'report', seg:'report/summary-table', feature:'reports', cluster:'report')이 이미 존재하며, 부과내역서·수납현황은 같은 탭 안에서 라우트만 갈아끼우는 내부 세그먼트(ReportKindTabsAto, §3)로 편입된다 — 탭 레일 단일화(IA #1) 정신에 따름.

1-2. 페이지 라우트 (buildLineRoutes.js)

editions.actual.ownreport/charge-statement 키 추가:

js
'report/charge-statement': () => import('../views/service-charge/actual/console/report/charge-statement/IndexView.vue')

provisionalown 매니페스트에는 이 키가 없다 — 라우트 자체가 생성되지 않는다(총괄표와 동일 게이트 패턴, 테스트: src/router/__tests__/buildLineRoutes.spec.js).

1-3. 인쇄 문서 라우트 (src/router/index.js)

두 라우트가 charge-summary-view(총괄표 인쇄) 뒤에 배선됐다:

/document/report/charge-statement-view  → views/document/report/ChargeStatementView.vue
/document/report/invoice-batch-view     → views/document/report/InvoiceBatchView.vue

document 앱 하위 라우트라 콘솔과 달리 feature 게이트가 없다(URL 직접 진입 시 항상 접근 가능 — 고지서 인쇄 뷰와 동형).

1-4. i18n

신규 키 없음. 보고서 종 세그먼트 라벨은 ReportKindTabsAto.vue 내부에 한국어 리터럴로 인라인(총괄표 뷰 토글 VIEWS 관례와 동일 — 컴포넌트 로컬 라벨은 i18n 대상 아님).


2. 데이터 컴포저블 — useChargeStatement

위치: src/composables/useChargeStatement.js. 순수 함수 컴포저블(라우터/컴포넌트 의존 없음).

API 시그니처:

  • unitOptions() → 세대 셀렉터용 배열. 각 원소는 unitCode·name.
  • statementFor(unitCode){ rows, 세전합계, 부가세, 당월합계 }. rows의 각 원소는 chargeCode·name·basis·unitPrice·quantity·amount·lines.
  • formatAmountuseAllocations에서 재-export된 금액 포맷 함수.

statementFor는 매 호출마다 activeCharges 전체를 순회하며 allocationBreakdown을 호출한다(세대 하나당 O(항목 수)) — 부과내역서 웹조회는 세대 1개만 렌더하므로 성능 이슈 없음. 단, ChargeStatementView.vue(인쇄 전세대 블록, §4)는 unitOptions().map(o => statementFor(o.unitCode))로 세대 수만큼 반복 호출한다 — 총괄표의 unitRows()와 동급 스캔 비용. 현재 규모에서 관측된 성능 문제 없음.


3. 보고서 종 세그먼트 — ReportKindTabsAto

위치: src/components/service-charge/actual/console/report/_shared/ReportKindTabsAto.vue. 총괄표·부과내역서·수납현황 3개 웹조회 화면 상단에 공통으로 배치되는 세그먼트 컴포넌트(탭 아님 — tabs-list 스타일을 쓰지만 라우트 전환 버튼).

js
const KINDS = [
  {
    key: "summary-table",
    label: "총괄표",
    path: "/service-charge/actual/console/report/summary-table",
  },
  {
    key: "charge-statement",
    label: "부과내역서",
    path: "/service-charge/actual/console/report/charge-statement",
  },
  {
    key: "collection-status",
    label: "수납현황",
    path: "/service-charge/actual/console/report/collection-status",
  },
];

route.path === k.path로 활성 여부 판정, 클릭 시 router.push(k.path). 탭 레일(consoleTabs)은 그대로 두고 내부 라우트만 전환하는 설계 — 3개 IndexView가 각각 이 컴포넌트를 최상단에 import해 렌더한다. 신규 보고서 종을 추가하려면 이 배열에 항목 하나를 더하면 3화면 전부에 자동 반영된다(각 MainOrg가 이미 이 컴포넌트를 공유하므로 개별 배선 불요).


4. 컴포넌트 구조

  • src/views/service-charge/actual/console/report/charge-statement/IndexView.vue — 콘솔 페이지 shell(공용 report/_shared/HeaderOrg[기간 헤더+콘솔 탭 레일] + padding + MainOrg — 2026-07-10 후속: 헤더 누락 정정). MainOrg 상단 [고지서] 버튼이 invoice-preview-art 오버레이(정본 InvoiceDynamicOrg + 선택 세대 sel 주입)를 연다.
  • src/components/service-charge/actual/console/report/charge-statement/MainOrg.vue — 웹조회: ReportKindTabsAto + 세대 셀렉터(<select v-model>) + 산출근거 표(table table-sm table-divide-y) + 인쇄 버튼 2개.
  • src/views/document/report/ChargeStatementView.vue — 인쇄 문서 뷰(전세대 연속 블록, document 앱).
  • src/views/document/report/InvoiceBatchView.vue — 고지서 묶음 인쇄 문서 뷰(신규 서식 0 — 기존 InvoiceDynamicOrg 재사용).

MainOrg는 read-only 조망이다. 세대 셀렉터는 네이티브 <select>(DS input input-bordered input-md) — 세대 수가 많아지면 combobox/검색형으로 교체 검토 대상이나 현재는 규모상 select로 충분.


5. 인쇄 뷰 패턴

5-1. ChargeStatementView.vue — 전세대 연속 블록, portrait flow

총괄표(§5-2 flow-scoped 패턴, report-summary-table.md 참고)와 동일한 print-a4-portrait-flow-scoped(폭 190mm = 210 − @page margin 10mm×2, 높이 auto)를 재사용한다. 신규 CSS는 .statement-block-scoped { break-inside: avoid; } 하나뿐 — 세대 블록(제목 + 표)이 페이지 경계에서 잘리지 않도록 보장. unitOptions().map(o => ({ ...o, statement: statementFor(o.unitCode) }))로 전세대를 미리 계산해 v-for로 순회 렌더한다.

5-2. InvoiceBatchView.vueInvoiceDynamicOrg 세대 순회, 신규 서식 0

html
<div v-for="u in units" :key="u.unitCode" class="invoice-page-scoped">
  <InvoiceDynamicOrg :sel="{ axis: 'unit', key: u.unitCode }" />
</div>

InvoiceDynamicOrg(src/components/document/transaction/InvoiceDynamicOrg.vue)에 신규 sel prop을 추가해 이 배치 인쇄가 가능해졌다:

js
const props = defineProps({ sel: { type: Object, default: null } });
const vm = computed(() =>
  invoiceFor(props.sel ?? (selectedBilling.value?.key ? selectedBilling.value : fallbackSel.value)),
);

우선순위: props.sel(외부 명시 지정, 묶음 인쇄용) → selectedBilling(콘솔에서 진입 시 선택 컨텍스트) → fallbackSel(데모 폴백, 첫 과세 계약) — 기존 단독 라우트(/document/transaction/invoice-dynamic-view, selectedBilling 경유 진입) 동작은 무변경. sel 미지정 시 기존 폴백 체인이 그대로 살아있어 회귀 없음.

각 인스턴스는 .invoice-page-scoped { break-after: page; }로 세대마다 새 페이지에서 시작한다. 이 문서 자체는 고지서 print CSS(고정 mm 그리드, print-a4-portrait 1장 전제)를 그대로 상속한다 — 총괄표·부과내역서의 flow-scoped 변형과 달리 세대당 정확히 1장이므로 flow 변형이 필요 없다.

5-3. 대량 인쇄 = 스코프 밖(문서류 매트릭스 참고)

두 인쇄 뷰 모두 세대수 비례 문서다. 브라우저 렌더 방식은 소규모(수십 세대)에서 문제없지만, 수백~수천 세대 대량 인쇄는 서버 njk SSR + Chromium 배치 파이프(REVIEW-INVOICE-MASS-PRINT-2026-07-10.md, docs/handoff/backend/invoice-print-pipeline.md)로 수렴해야 한다 — 이 트랙은 검토·산출물(템플릿·스크립트)까지 완료됐고 콘솔 실배선은 스코프 밖. InvoiceBatchView.vue의 no-print 안내 문구("브라우저 인쇄(대량 발송분은 PDF 파이프라인 예정)")가 이 상태를 화면에 정직하게 노출한다.


6. [시트] — xlsxExport 유틸 계약 (2026-07-11 후속, B-3)

js
import { periodOf, periodLabelOf } from "@/composables/useBillingPeriod";
import { exportXlsx } from "@/lib/report/xlsxExport";

const XLSX_COLUMNS = [
  { key: "name", label: "항목" },
  { key: "basis", label: "배분기준" },
  { key: "unitPrice", label: "단가(원)" },
  { key: "quantity", label: "수량" },
  { key: "amount", label: "금액(원)" },
];
const selectedUnitName = computed(
  () => options.value.find((o) => o.unitCode === selectedUnit.value)?.name ?? selectedUnit.value,
);
const downloadXlsx = () =>
  exportXlsx({
    fileName: `부과내역서-${periodLabelOf("service-charge", "actual")}-${selectedUnitName.value}.xlsx`,
    sheetName: "부과내역서",
    columns: XLSX_COLUMNS,
    rows: statement.value.rows,
  });
  • exportXlsx({ fileName, sheetName, columns, rows })(src/lib/report/xlsxExport.js, SheetJS write-only) — 부과내역서만 유일하게 파일명에 세대명이 붙는다(부과내역서-2025년12월-101호.xlsx) — 다른 4보고서(총괄표·수납현황·산출표·부과요약서)는 전세대/전항목 단일 시트라 세대 표기가 없다. rows지금 선택된 세대(selectedUnit) 기준만 export한다 — 세대를 바꾸고 [시트]를 다시 눌러야 다른 세대 파일이 나온다(전세대 일괄 export는 미제공, ChargeStatementView.vue의 인쇄 뷰만 전세대를 다룬다).
  • 값 규율: 숫자는 raw number로 기록(화면 포맷 문자열 금지) — statement.value.rows(컴포저블 원본)를 그대로 넘기므로 자동으로 지켜진다.
  • 배선 스모크: src/components/service-charge/actual/console/report/__tests__/XlsxWiring.spec.js — 5보고서 공통.

7. 알려진 미해결 사항 (스코프 밖, 회귀 아님)

  • document 앱 셸 no-print 갭(총괄표 §6와 동일 — AppAsideOrg.vue에 no-print 래핑 없음)은 이 두 문서에도 동일하게 적용된다. 별도 회귀 아님, 손대지 않았다.
  • 세대 셀렉터가 select 하나뿐이라 검색/필터가 없다 — 세대 수가 많아지면 UX 개선 검토 대상(별도 후속).

8. 테스트 위치

  • src/composables/__tests__/useChargeStatement.spec.js — 불변식 4종(§ backend handoff §3 대응).
  • src/router/__tests__/buildLineRoutes.spec.js — actual 전용 라우트 게이트(provisional 미생성).
  • src/components/service-charge/actual/console/report/__tests__/XlsxWiring.spec.js[시트] 배선 스모크(부과내역서 케이스 포함).
  • Playwright 라이브 실측(비상시 vitest 미커버): 세대 선택 변경 시 표 갱신, [시트] 다운로드, 인쇄 뷰 3종(부과내역서·고지서 묶음·수납현황) 개봉 — SDD plan §검증 절 참고.

9. 확장 절차

새 보고서 종을 이 보고서 탭에 추가하는 일반 절차는 docs/handoff/frontend/report-summary-table.md §8을 참고 — 본 문서(②)와 수납현황(③) 모두 그 절차를 그대로 따라 만들어졌다(3단계 "데이터 컴포저블 신설"이 useChargeStatement, 4단계 "웹조회 컴포넌트"가 이 MainOrg.vue).

2026-07-10 후속 2 (SSOT 정합): 웹조회 MainOrg는 leysys-design 정본 pattern-app/transaction/table/static-table 패턴을 따른다 — container 루트 > 보고서 종 세그먼트(page-header-below p-1 bg-neutral-minimal rounded-full 필 스트립) > card card-md(card-header bg-neutral-minimal inset-edge-b 툴바 + card-body bg-neutral-minimal 표). 테이블은 정본 static 계열 table table-hover table-sm table-static table-divide-y(장표는 +sticky-thead). 미수금 연령분석 축탭도 동일 필 스트립·h-fit 래퍼(flex shrink 압축 방지).