다크모드
관리비 총괄표 — FE 메인테이너 참고
관리비 상용화 필수 문서 트랙(펀치리스트 점검 4번)의 1호. 설계 정본: docs/superpowers/specs/2026-07-10-charge-summary-table-design.md. 데이터 계약은 docs/handoff/backend/report-summary-table.md 참고(본 문서는 라우트, 게이트, 컴포넌트, 인쇄 뷰 중심).
1. 라우트 / feature 게이트
1-1. 콘솔 탭 (billingLines.js)
service-charge.editions.actual.features 배열: notice, advanceReceipt, forwarding, reports.
consoleTabs 배열에 tax-invoice 뒤로 다음 항목 추가: group: report, labelKey: billing.tabs.report, seg: report/summary-table, feature: reports, cluster: report.
- reports feature는 actual 에디션에만 존재한다 — provisional features 배열에는 없다. 탭 게이트(consoleTabs)와 라우트 게이트(buildLineRoutes.js 매니페스트)가 동반되어야 provisional에서 탭 숨김과 라우트 부재가 함께 성립한다(과거 이월 탭에서 탭은 숨겼지만 라우트가 남아 눌러도 빈 화면이 뜨던 결함의 교훈 — C-4).
- cluster: report는 ConsoleTabBarOrg의 시각 구분선(그룹핑)일 뿐이다. src/composables/tests/billingLinesClusters.spec.js의 CLUSTERS 배열(overview, process, receivable, tax, report)에 등록돼 있어야 클러스터 검증 테스트가 통과한다.
1-2. 페이지 라우트 (buildLineRoutes.js)
editions.actual.own에 report/summary-table 키를 추가하고 views/service-charge/actual/console/report/summary-table/IndexView.vue를 동적 import한다.
provisional의 own 매니페스트에는 이 키가 없다 — 라우트 자체가 생성되지 않는다(테스트: src/router/tests/buildLineRoutes.spec.js, 이월/forwarding 게이트 테스트를 미러한 패턴).
1-3. 인쇄 문서 라우트 (src/router/index.js)
경로 /document/report/charge-summary-view — document 앱 하위 라우트로, invoice-dynamic-view(고지서 인쇄 뷰) 바로 뒤에 배선됐다. 콘솔 라우트와 달리 이쪽엔 feature 게이트가 없다 — document 앱은 URL 직접 진입 시 항상 접근 가능한 구조(고지서 인쇄 뷰와 동형).
1-4. i18n
billing.tabs.report = 보고서(ko) / Reports(en) — localeParity.spec.js가 ko/en 누락을 강제하므로 신규 라벨 추가 시 항상 양쪽 동시 추가.
2. 데이터 컴포저블 — useChargeSummaryTable
위치: src/composables/useChargeSummaryTable.js. 순수 함수 컴포저블(라우터/컴포넌트 의존 없음 — 어디서든 호출 가능).
API 시그니처:
- chargeColumns() → 활성 항목 순서 배열, 각 원소는 chargeCode와 name 필드를 가진다.
- unitRows() → 유닛별 행 배열. 각 행은 unitCode, unitName, cells(chargeCode별 금액 맵), 세전합계, 부가세, 당월합계, 미납원금, 미납연체금, 선수금, 청구액 필드를 가진다.
- columnTotals(rows) → rows 인자를 생략하면 내부에서 unitRows()를 다시 호출한다. 이미 rows를 갖고 있으면 반드시 명시 전달할 것(중복 계산 방지, 2-1 참고).
- totalExclusiveArea() → 당기 부과 세대 exclusiveArea 합(숫자).
- kapt() → rows, vat, grandTotal, totalArea 필드를 가진 객체. rows의 각 원소는 chargeCode, name, total, unitPrice.
- formatAmount → useAllocations에서 재-export된 금액 포맷 함수.
2-1. 호출 관용 — rows를 한 번만 계산해서 재사용
unitRows()는 매 호출마다 invoicingByUnit(), cellMatrix(), invoiceFor()를 N회 다시 돈다. 컴포넌트에서는 computed로 한 번 감싸고, columnTotals(rows.value)처럼 명시 전달해야 한다. columnTotals()를 인자 없이 호출하면 내부에서 unitRows()를 또 호출해 중복 스캔이 발생한다.
kapt()는 rows 파라미터가 없다 — 내부적으로 columnTotals()를 인자 없이 호출한다(즉 unitRows()를 한 번 더 돈다). 이는 Task 1 브리프에서 승인된 유일한 예외이며, 현재 규모에서 성능 이슈로 관측되지 않았다 — 최적화가 필요해지면 kapt에 rows 파라미터를 추가하는 방식을 고려한다.
3. 뷰 키 어휘
세 화면(웹조회 MainOrg.vue, 인쇄 ChargeSummaryView.vue) 모두 동일한 뷰 키 3종을 공유한다 — 이 문자열 리터럴이 계약이므로 신규 코드에서 임의 변형하지 말 것.
| 키 | 라벨 | 용지 방향(인쇄) |
|---|---|---|
| unit-by-charge | 세대 x 항목 | landscape |
| charge-by-unit | 항목 x 세대(전치) | landscape |
| kapt | K-apt 공시형 | portrait |
웹조회(MainOrg.vue)의 인쇄 버튼은 현재 활성 뷰를 그대로 쿼리로 전달한다: router.push({ path: '/document/report/charge-summary-view', query: { view: view.value } }).
인쇄 뷰(ChargeSummaryView.vue)는 쿼리를 화이트리스트 검증하고, 유효하지 않으면 unit-by-charge로 폴백한다.
4. 컴포넌트 구조
- src/views/service-charge/actual/console/report/summary-table/IndexView.vue — 콘솔 페이지 shell(공용 report/_shared/HeaderOrg[기간 헤더+콘솔 탭 레일] + MainOrg — 2026-07-10 후속: 헤더 누락 정정. 연령분석(receivable-aging)도 동일 정정·축탭은 blocks/TabBarOrg로 이동).
- src/components/service-charge/actual/console/report/summary-table/MainOrg.vue — 웹조회: 뷰 토글 + 3종 table + 인쇄 버튼.
- src/views/document/report/ChargeSummaryView.vue — 인쇄 문서 뷰(document 앱, 고지서 InvoiceDynamicView 동형 패턴).
MainOrg.vue는 read-only 조망이다. 스테이지 위젯 없이 경량 헤더(제목+기간 문구)만 사용한다 — 브리프 3절 "보고서는 read-only 조망" 결정에 따름.
웹조회 표는 DS table table-sm table-divide-y sticky-thead에 sticky-col과 sticky-col-end(가로 스크롤 중 세대/합계 열 고정)를 더한다. 인쇄 뷰는 sticky 계열 클래스를 뺀 동일 table table-sm table-divide-y를 쓴다(인쇄에서는 브라우저가 thead를 페이지마다 자동 반복하므로 sticky가 무의미).
뷰 토글 UI는 DS tabs-list tabs-list-primary-moderate tabs-list-radius-full 세그먼트(3버튼).
5. 인쇄 뷰 패턴 — named at-page landscape + flow-scoped 컨테이너
ChargeSummaryView.vue는 고지서(InvoiceDynamicView.vue) 인쇄 패턴을 계승하되, 두 가지를 새로 도입했다.
5-1. Named at-page로 문서별 용지 방향 전환
전역 print-layout.css가 페이지 규칙을 A4 portrait로 고정하고 있어, 이 문서(가로 표)만 landscape로 바꾸려면 named page를 정의하고 page CSS 속성으로 특정 컨테이너에 지정해야 한다. charge-summary-landscape라는 이름의 페이지 규칙을 size A4 landscape margin 10mm로 선언하고, 컨테이너 셀렉터에서 이 named page를 참조한다(has 셀렉터로 landscape-flow 클래스를 가진 자손이 있을 때만 적용).
Chromium에서 실측 검증됨(2026-07-10, Playwright headless page.pdf preferCSSPageSize true — 출력 MediaBox 297x210mm 확인). 비Chromium 엔진은 전역 페이지 규칙(portrait)으로 저하되어 세로로 인쇄되되 파손되지는 않는다(가로 297mm 표가 210mm 폭 밖으로 넘칠 수 있음) — 프로토 타깃(시스템 Chrome, REVIEW-INVOICE-MASS-PRINT-2026-07-10.md 8절)이 이미 확인됐으므로 게이트하지 않았다. 크로스브라우저 검증이 필요해지면 별도 확인 필요.
2026-07-10 정정(T5·보고서②③ 선행 태스크):
.print-a4-landscape-flow-scoped컨테이너 폭이 최초 구현 시 named@page(landscape 297mm) 전폭과 동일하게 297mm로 잡혀 있어@page margin 10mm×2를 차감하지 않은 채였다 — 실인쇄 시 표가 지면 여백 밖으로 우측 넘침(Playwright 실측 테이블 실폭 273mm).size A4 landscape(297mm) −margin 10mm×2= 277mm로 정정(ChargeSummaryView.vuescoped 스타일, 재배치 없음 — 폭 숫자만 교체). 신규 세로 인쇄 뷰(부과내역서·수납현황,report-charge-statement.md·report-collection-status.mdFE handoff)는 처음부터 같은 계산(210 − 10mm×2 = 190mm)으로 설계돼 동일한 함정을 재현하지 않았다.
5-2. flow-scoped 컨테이너 — 고정 높이 A4 클래스와 구분
기존 print-a4-landscape / print-a4-portrait(src/assets/styles/print/print-layout.css)는 높이 고정(297mm/210mm, 1장 전제 — 고지서처럼 세대당 정확히 1장인 문서용)이다. 총괄표는 세대 수에 따라 여러 장으로 흐를 수 있는 표라서, 폭만 고정하고 높이는 auto인 신규 변형(print-a4-landscape-flow-scoped, print-a4-portrait-flow-scoped)을 이 컴포넌트 scoped 스타일에 정의했다.
scoped 접미사는 CLAUDE.md 안의 style scoped 네이밍 규칙 준수(글로벌 DS 클래스와 컴포넌트 로컬 클래스 구분). 디자인 시스템에 정착시킬 만큼 반복되면(다른 보고서도 다장 표를 인쇄하게 되면) leysys-design SSOT에 flow 변형으로 승격을 검토할 것 — 현재는 소비처가 하나뿐이라 ad-hoc으로 유지한다(Token to Component to Pattern to Page 순서상, 반복 전까지는 승격 보류가 맞는 판단).
5-3. 테이블 밀도 — DS table 어휘(InvoiceTable 계열 아님)
print-table-unscoped / InvoiceTable 계열은 고지서 전용 고정 mm 그리드(print-a4-portrait 고정 높이 1장 전제)라 이 문서에는 부적합하다고 판단해 채택하지 않았다. 대신 웹조회(MainOrg.vue)와 동일한 table table-sm table-divide-y를 인쇄 뷰에도 그대로 사용한다 — print-layout.css의 표 셀 패딩 축소 규칙이 이미 인쇄 밀도를 보정해준다. sticky 계열 클래스는 인쇄에서 의미가 없어 제거했다.
5-4. 숫자 정본은 한 곳
화면(MainOrg.vue)과 인쇄(ChargeSummaryView.vue) 둘 다 useChargeSummaryTable을 직접 호출한다 — 별도 인쇄 전용 VM이나 재계산 레이어가 없다(REVIEW-INVOICE-MASS-PRINT-2026-07-10.md 5절 규칙 1과 동형). 두 렌더링이 다른 숫자를 보여줄 여지가 구조적으로 없다.
6. [시트] — xlsxExport 유틸 계약 (2026-07-11 후속, B-3)
js
import { exportXlsx } from "@/lib/report/xlsxExport";
const xlsxColumns = computed(() => [
{ key: "unitName", label: "세대" },
...columns.value.map((c) => ({ key: `charge:${c.chargeCode}`, label: c.name })),
{ key: "세전합계", label: "세전합계" },
{ key: "부가세", label: "부가세" },
{ key: "당월합계", label: "당월합계" },
{ key: "미납원금", label: "미납원금" },
{ key: "미납연체금", label: "미납연체금" },
{ key: "선수금", label: "선수금" },
{ key: "청구액", label: "청구액" },
]);
const xlsxRows = computed(() =>
rows.value.map((r) => ({
...r,
...Object.fromEntries(
columns.value.map((c) => [`charge:${c.chargeCode}`, r.cells[c.chargeCode] ?? 0]),
),
})),
);
const downloadXlsx = () =>
exportXlsx({
fileName: `총괄표-${periodLabelOf("service-charge", "actual")}.xlsx`,
sheetName: "총괄표",
columns: xlsxColumns.value,
rows: xlsxRows.value,
});exportXlsx({ fileName, sheetName, columns, rows })(src/lib/report/xlsxExport.js) — SheetJS write-only(파일 read 경로 없음)이며[시트]클릭 시 동적 import한다.[시트]버튼은 현재 활성 뷰와 무관하게 세대×항목 뷰(unit-by-charge) 기준 데이터를 항상 export한다 — 뷰 토글(항목×세대·K-apt)은 화면 표시 방식만 바꿀 뿐 같은 숫자이므로(§본 문서 서두 "뷰 3종은 같은 숫자를 다른 각도로" 원칙), export 대상을 굳이 뷰별로 분기하지 않았다.- 값 규율: 숫자는 raw number로 기록(화면 포맷 문자열 금지) —
rows.value(컴포저블 원본)를 그대로 넘기므로 자동으로 지켜진다. - 동적 열 키 격리: 항목 열은 export 전용
charge:<chargeCode>키를 사용한다. 따라서 향후chargeCode가부가세·청구액같은 고정 요약 키와 같아져도 동적 금액과 요약 금액이 서로 덮어쓰지 않는다. 배선 테스트는 열 키 유일성과 모든 동적 열의 행 값 존재를 검증한다. - 파일명 규약:
총괄표-<periodLabel>.xlsx(세대 표기 없음 — 전세대 단일 시트이므로 부과내역서와 달리-<unitName>접미사가 붙지 않는다). - 배선 스모크:
src/components/service-charge/actual/console/report/__tests__/XlsxWiring.spec.js—[시트]버튼 클릭 시exportXlsx가 화면 rows로 1회 호출되는지 5보고서 공통 검증.
7. 알려진 미해결 사항 (스코프 밖, 회귀 아님)
document 앱 셸에 no-print 래퍼가 없다: AppAsideOrg.vue(document 앱 좌측 메뉴)가 no-print로 감싸져 있지 않아, SPA 안에서 직접 인쇄 단축키를 누르면 좌측 메뉴까지 인쇄될 수 있다. 이는 InvoiceDynamicView.vue(고지서, 레퍼런스 패턴)도 동일하게 갖고 있는 기존 갭이며 이번 작업이 만든 회귀가 아니다 — 손대지 않았다. document 앱 전체에 no-print 셸 래핑이 필요해지면 별도 트랙.
(해결됨, 기록용) 인쇄 flow 폭 우측 여백 타이트 이슈(landscape 컨테이너 297mm 전폭 — @page margin 미차감)는 §5-1 정정으로 2026-07-10 해소됐다(297mm → 277mm). 더 이상 미해결 항목 아님 — 이력 참고용으로만 남김.
8. 확장 절차 — 다음 보고서를 같은 보고서 탭에 추가하기
2026-07-10 갱신(보고서②③ 완료 반영): 아래 1단계 "탭 등록"은 최초 1회만 해당한다 — 부과내역서(②)·수납현황(③) 추가 시에는 신규 consoleTabs 항목을 만들지 않고, 기존 보고서 탭 하나(
group:'report', seg:'report/summary-table') 아래 내부 세그먼트로 편입했다(§3 참고). 실제로 만들어진 컴포넌트는src/components/service-charge/actual/console/report/_shared/ReportKindTabsAto.vue— 총괄표·부과내역서·수납현황 3개 MainOrg가 최상단에 공통으로 import해서 쓰는 라우트 전환 세그먼트다(tabs-list스타일,router.push(path)로 3개 콘솔 라우트를 오간다). 신규 보고서 종을 또 추가할 때는 1단계(탭 등록)를 건너뛰고,ReportKindTabsAto.vue의KINDS배열에{ key, label, path }한 줄을 추가하면 3개(이제는 N개) 화면 전부에 자동 반영된다. 데이터 계약·인쇄 뷰 예시는docs/handoff/frontend/report-charge-statement.md·report-collection-status.md참고.
- 탭 등록(최초 1회만 — §3 참고): billingLines.js consoleTabs에 group report, seg report/새경로, feature reports, cluster report 항목을 추가한다(기존 reports feature 재사용 — 신규 feature 불요, 단 provisional 배제가 필요 없다면 provisional features에도 추가 검토). 두 번째 보고서부터는 이 단계 생략 — 대신
ReportKindTabsAto.vue의KINDS배열에 항목 추가. - 라우트 등록: buildLineRoutes.js의 editions.actual.own(또는 필요 에디션)에 report/새경로 키를 추가한다.
- 데이터 컴포저블 신설: useChargeSummaryTable을 재사용하지 말고 새 보고서 전용 컴포저블을 만들되, 재계산 금지 원칙(백엔드 문서 1절 — 기존 캐논 조합만)을 그대로 따른다.
- 웹조회 컴포넌트: components/service-charge/actual/console/report/새경로/MainOrg.vue — 최상단에
ReportKindTabsAto를 배치하고, 그 아래는 이 총괄표 MainOrg.vue를 구조 레퍼런스로 삼는다(뷰 토글이 필요 없으면 생략 가능). - 인쇄가 필요하면: views/document/report/새문서View.vue 신설과 router/index.js에 라우트 등록. 다장 표라면 5-2의 flow-scoped 패턴을(폭은 세로 190mm·가로 277mm 산식 — margin 10mm×2 차감을 반드시 반영, §5-1 정정 사례 참고), 단일 페이지라면 기존 print-a4-landscape / print-a4-portrait를 그대로 쓴다.
- i18n: 신규 탭 라벨 ko/en 동시 추가(localeParity.spec.js 강제). 단, 1단계를 생략(두 번째 보고서부터)한 경우 신규 consoleTabs 라벨 자체가 없으므로 이 단계도 불요 —
ReportKindTabsAto의 세그먼트 라벨은 컴포넌트 로컬 한국어 리터럴이라 i18n 대상이 아니다. - route integrity: node scripts/audit-route-integrity.mjs로 nav 참조 무결성 확인.
- 문서 3종 + docs 미러: CLAUDE.md 페이지 작업 3종 문서 의무에 따라 manual, backend, frontend 3파일을 같은 작업에서 작성하고 leyve-docs-proto에 미러한다.
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 압축 방지).