다크모드
산출표 — FE 메인테이너 참고
관리비 상용화 보고서 확충 트랙(2026-07-11, B-1). 설계 정본:
docs/superpowers/specs/2026-07-11-period-tag-minors-reports-xlsx-design.md§B-1. 데이터 계약은docs/handoff/backend/report-calculation-table.md참고. 총괄표 FE handoff(report-summary-table.md) §1~§8 패턴을 그대로 복제 — 본 문서는 산출표 고유 지점만 서술.
1. 라우트 / feature 게이트
1-1. 콘솔 탭
신규 탭 없음 — 기존 보고서 탭(feature:'reports')의 내부 세그먼트(ReportKindTabsAto, §3)로 편입. KINDS 배열의 맨 앞에 추가돼 산출표가 보고서 세그먼트 중 가장 왼쪽에 위치한다.
1-2. 페이지 라우트 (buildLineRoutes.js)
editions.actual.own에 report/calculation-table 키:
js
'report/calculation-table': () => import('../views/service-charge/actual/console/report/calculation-table/IndexView.vue')provisional에는 없음 — 라우트 게이트(테스트: buildLineRoutes.spec.js).
1-3. 인쇄 문서 라우트
/document/report/calculation-table-view → views/document/report/CalculationTableView.vuefeature 게이트 없음(document 앱 공통 패턴).
1-4. i18n
신규 키 없음(ReportKindTabsAto의 세그먼트 라벨은 컴포넌트 로컬 리터럴).
2. 데이터 컴포저블 — useCalculationTable
위치: src/composables/useCalculationTable.js. 순수 함수 컴포저블(라우터/컴포넌트 의존 없음).
API 시그니처:
calculationRows()→ 활성 항목 배열. 각 원소는chargeCode·name·chargeBasis·수량·단가·발생금액·할인금액·부과금액.calculationTotals(rows = calculationRows())→발생금액·할인금액·부과금액합산 객체.rows인자를 생략하면 내부에서calculationRows()를 재호출(총괄표columnTotals관용과 동일 — 명시 전달 권장).formatAmount→useCharges에서 재-export.
3. 보고서 종 세그먼트 — ReportKindTabsAto
부과내역서 handoff §3과 동일 컴포넌트(src/components/service-charge/actual/console/report/_shared/ReportKindTabsAto.vue) — KINDS 배열에 { key: 'calculation-table', label: '산출표', path: '...report/calculation-table' } 한 줄이 맨 앞에 추가됐다(신규 보고서 종 추가 시 배열 순서 = 화면 노출 순서). 현재 5종: 산출표 → 총괄표 → 부과내역서 → 부과요약서 → 수납현황.
4. 컴포넌트 구조
src/views/service-charge/actual/console/report/calculation-table/IndexView.vue— 콘솔 페이지 shell(공용report/_shared/HeaderOrg+ MainOrg).src/components/service-charge/actual/console/report/calculation-table/MainOrg.vue— 웹조회:ReportKindTabsAto+ 항목별 표(table table-hover table-sm table-static table-divide-y sticky-thead) +[시트][인쇄]버튼.src/views/document/report/CalculationTableView.vue— 인쇄 문서 뷰.
MainOrg는 read-only 조망. 화면·인쇄 양쪽 모두 useCalculationTable을 직접 호출한다(별도 인쇄 전용 VM 없음 — 숫자 정본 한 곳).
5. 인쇄 뷰 패턴
CalculationTableView.vue는 총괄표·부과내역서·수납현황과 동일한 print-a4-portrait-flow-scoped(폭 190mm = 210 − @page margin 10mm×2, 높이 auto)를 재사용한다. 열이 7개로 세로 용지에 여유가 있어 landscape 전환이 필요 없다. 신규 CSS 없음(기존 flow-scoped 클래스 그대로 컴포넌트 scoped 스타일에 재선언).
6. [시트] — xlsxExport 유틸 계약
js
import { exportXlsx } from "@/lib/report/xlsxExport";
const XLSX_COLUMNS = [
{ key: "name", label: "항목" },
{ key: "chargeBasis", label: "부과기준" },
{ key: "수량", label: "수량" },
{ key: "단가", label: "단가(원)" },
{ key: "발생금액", label: "발생금액" },
{ key: "할인금액", label: "할인금액" },
{ key: "부과금액", label: "부과금액" },
];
const downloadXlsx = () =>
exportXlsx({
fileName: `산출표-${periodLabelOf("service-charge", "actual")}.xlsx`,
sheetName: "산출표",
columns: XLSX_COLUMNS,
rows: rows.value,
});exportXlsx({ fileName, sheetName, columns, rows })(src/lib/report/xlsxExport.js) — SheetJS(xlsx패키지) write-only 사용(파일 파싱/read 경로 없음 — read-path 취약점 비해당).columns는{ key, label }배열,rows는 화면 rows 그대로(별도 산식 없음 — 화면=시트 동치).- 값 규율: 숫자는 raw number로 기록한다(화면 포맷 문자열
fmt()결과를 넣지 않음) — 엑셀에서 바로 합산 가능해야 하기 때문.rows.value를 그대로 넘기므로 이 규율은 컴포저블 반환값이 이미 숫자 타입이기만 하면 자동으로 지켜진다(별도 변환 불요). - 파일명 규약:
<보고서명>-<periodLabel>.xlsx(세대 선택이 있는 부과내역서만-<unitName>추가,report-charge-statement.md§xlsx 참고).
7. 알려진 미해결 사항
- document 앱 셸 no-print 갭 — 총괄표 §6와 동일 근거로 스코프 밖.
- 기간범위(월 구간)·층/블록 축 필터 없음 — backend handoff §5-6 참고.
8. 테스트 위치
src/composables/__tests__/useCalculationTable.spec.js— 불변식 3종(backend handoff §3 대응).src/router/__tests__/buildLineRoutes.spec.js— actual 전용 라우트 게이트.src/components/service-charge/actual/console/report/__tests__/XlsxWiring.spec.js— 5보고서[시트]배선 스모크(산출표 케이스 포함) — 버튼 클릭 시exportXlsx가 화면rows로 1회 호출되는지 확인.- Playwright 라이브 실측: 세그먼트 진입 순서(맨 앞),
[시트]다운로드, 인쇄 뷰 개봉.
9. 확장 절차
새 보고서 종을 추가하는 일반 절차는 docs/handoff/frontend/report-summary-table.md §8(8단계)을 참고 — 본 문서(산출표)는 1단계(탭 등록)를 건너뛰고 ReportKindTabsAto의 KINDS 배열에 항목만 추가해 만들어졌다(3단계 "데이터 컴포저블 신설"이 useCalculationTable, 4단계가 이 MainOrg.vue).