다크모드
FE 메인테이너 — 미수금 연령분석(AR Aging)
독자: FE 메인테이너/AI. "이 화면을 이어 개발하려면 무엇을 알아야 하나"를 정의한다. 정본 spec:
docs/superpowers/specs/2026-06-29-receivable-aging-carryover-canon-design.md. BE 계약:docs/handoff/backend/receivable-aging.md.
1. 라우트 · 진입
- 경로:
/service-charge/actual/console/forwarding/brought-forward - 에디션 제한:
actual전용 —provisional에디션에서는 이 라우트를 메뉴에 노출하지 않는다. - 뷰:
src/views/billing/_core/console/forwarding/brought-forward/IndexView.vue—MainOrg를 full-height 컨테이너에 단순 렌더한다. - 역할: read-only 조망 화면. 액션(수납·조정·생성) 없음. 시트 오버레이 없음.
2. 컴포넌트 트리
IndexView.vue
└─ MainOrg.vue (축 state ref `axis` ∈ contract|unit|member, computed rows/buckets/summary)
├─ HeaderOrg.vue (제목 "미수금 연령분석" + 3-축 탭 — 계약/유닛/멤버)
├─ SummaryStripMol.vue (요약 스트립 — 총차기이월·전기이월·당기발생·연체행수·최장연체차수)
├─ card > AgingTableOrg.vue (행 테이블 — 대상명 클릭 → 차수 인라인 펼침)
└─ BucketStripOrg.vue (차수별 미수 분포 disclosure)파일 위치: src/components/billing/_core/console/receivable-aging/.
컴포넌트별 요약
| 컴포넌트 | 파일 | Props | 이벤트 |
|---|---|---|---|
| MainOrg | MainOrg.vue | — | — |
| HeaderOrg | HeaderOrg.vue | axis: String | update:axis |
| SummaryStripMol | blocks/SummaryStripMol.vue | summary: Object | — |
| AgingTableOrg | blocks/AgingTableOrg.vue | rows: Array | — |
| BucketStripOrg | blocks/BucketStripOrg.vue | buckets: Array | — |
3. 소비 컴포저블 · 데이터 흐름
컴포저블 체인
useReceivableAging() (src/composables/useReceivableAging.js)
└─ useForwarding() (내부: forwardingByContract/Unit/Member, isAnomalous)useReceivableAging은 useForwarding 위의 thin read-only 집계 레이어다. useForwarding을 직접 편집하지 않는다.
API 형태
| 함수 | 반환 | 설명 |
|---|---|---|
agingByAxis(axis) | AgingRow[] | 축별 행 목록 — useForwarding 행을 표준어 필드로 변환 |
agingBuckets(axis) | BucketEntry[] | 전 행 periods를 차수(label) 기준으로 group-by 합산 — 오래된→최근 정렬 |
agingSummary(axis) | AgingSummary | 집계 통계 — 총차기이월·연체행수·최장연체차수 등 |
AgingRow 필드
js
{
key: string, // 축 식별자 (contractCode|unitCode|memberCode)
name: string, // 표시명
전기이월: number, // forwarding 행의 forwarding (이월차수 미납 잔액)
당기발생: number, // 당기 차수 billed (periods.find(kind='current').billed)
회수: number, // 당기 차수 collected (kind='current' only)
조정: number, // 당기 차수 adjusted (kind='current' only)
차기이월: number, // forwarding 행의 total (마감 미수 잔액)
연체차수: number, // forwarding 행의 overdueCount
anomalous: boolean, // isAnomalous(r) — 배지 표시 여부
periods: Period[] // 원본 periods (차수 펼침에 직접 사용)
}BucketEntry 필드
js
{ label: string, outstanding: number, billed: number, collected: number, count: number }AgingSummary 필드
js
{
총전기이월: number, 총당기발생: number, 총회수: number, 총조정: number, 총차기이월: number,
미수행수: number, // 차기이월 > 0인 행 수
연체행수: number, // anomalous 행 수
최장연체차수: number
}데이터 흐름
MainOrg
axis (ref, 기본 'contract')
↓
agingByAxis(axis) → rows → AgingTableOrg
agingBuckets(axis) → buckets → BucketStripOrg
agingSummary(axis) → summary → SummaryStripMol
useReceivableAging.agingByAxis(axis)
rowsOf(axis) = fwd[AXES[axis]]() // forwardingByContract/Unit/Member
.map(toAgingRow)
→ r.forwarding → 전기이월
→ r.total → 차기이월
→ periods.find(kind='current').{billed, collected, adjusted} → 당기발생/회수/조정
→ fwd.isAnomalous(r) → anomalous회수·조정은 당기 차수(kind='current')에서만 읽는다. 전기이월에는 이미 이월차수 충당분이 반영돼 있으므로 이월 차수의 회수를 재차 빼면 중복차감된다. 유지보수 노트: 회수 컬럼 = 당기 차수 회수만 집계한다(전기이월 차수에 FIFO 충당된 회수는 전기이월 잔액에 이미 반영 → 회수 컬럼 미포함). Track 2 고지서가 이 수치를 투영할 때 "회수 = 당기 회수"임에 유의.
4. UI 동작
축 전환
HeaderOrg의 탭 클릭 → MainOrg의 axis ref 변경 → rows/buckets/summary computed 재파생. 라우트 변경 없음.
차수 펼침
AgingTableOrg 내부 expanded ref(한 번에 하나). 대상명 버튼 클릭 → toggle(r.key) → r.periods 인라인 전개(차수별 label·kind·dueDate·outstanding·status). 클릭 다시 하면 접힘.
연체 배지
anomalous === true 행의 연체차수 셀 → badge-sm badge-error-moderate로 강조. 나머지는 숫자만.
차수별 미수 분포
BucketStripOrg — disclosure 컴포넌트(open 기본 펼침). 내부 테이블에 max-h-64 overflow-y-auto + sticky-thead(내부 스크롤).
5. DS 패턴
- 페이지 레이아웃 =
container mx-auto h-full flex flex-col gap-4(MainOrg). - 요약 스트립 =
card card-sm card-inset-edged card-filled— 비접힘 평면 행(카드 규율3). - 행 테이블 카드 래퍼 =
card card-md > card-body bg-neutral-minimal overflow-x-auto. - 행 테이블 =
table table-sm table-static table-divide-y table-hover whitespace-nowrap sticky-thead. - 차수 펼침 내부 중첩 테이블 =
bg-neutral-subtletd +table table-sm table-static table-divide-y. - 차수 버킷 =
bg-neutral-minimal > disclosure disclosure-md disclosure-inset-edged disclosure-divide-y. - 대상명 =
font-medium cursor-pointer hover:underline(button 시맨틱) — 상시 파랑 링크색 없음.
6. 역할 경계
| 화면 | 역할 |
|---|---|
| 미수금 연령분석 (이 화면) | 미수 현황 read-only 조망 — 전기이월·당기발생·차기이월·차수 분포 |
| 수납 콘솔 | 회수 액션 — 수납 입력·충당·조정 |
| 계정 허브 CarryoverTab | 계정 단위 드릴다운 — 전표별 상세 |
| 청구 상세 시트 | 청구기 고지 맥락 — 당기분·이월분·분개 |
aging 행에서 수납·계정·청구 드릴다운으로 연결하는 액션은 현재 미구현(OUT). 추후 대상명 클릭을 수납 콘솔 행 딥링크 또는 계정 허브 시트 연결로 확장할 수 있다.
7. 게이팅
useModuleSubscription게이팅 없음 — 화면 전체가 read-only 집계라 별도 게이트 불필요.actual에디션 라우트에만 메뉴 노출.provisional미수 aging은 현재 범위 밖.
8. 확장 포인트
- roll-forward 런타임 엔진 (❌예정): 월말 미수→차월 이월 자동승격 처리. 도입 시
useForwarding의 정적 priorPeriods 시드를 월 경계 모델로 교체 + 차기이월 처리 로직 추가.useReceivableAging자체는useForwardingAPI만 소비하므로 변경 없음 또는 최소. - 드릴다운 배선: 대상명 클릭 → 수납 콘솔 해당 행 또는 계정 허브 시트로 연결.
router.push+ 쿼리 파라미터(?key=…&axis=…). - 날짜 기준 필터: 조회 기준일(타임머신)을
useForwarding에 전달해 기준일 시점의 aging 보기. 수납 콘솔의 기준일 패턴 참고. - PDF 내보내기 / 인쇄:
AgingTableOrg전체 행을 PDF로 출력하는 버튼. 현재 범위 밖.
9. 테스트 위치
| 대상 | 테스트 파일 |
|---|---|
agingByAxis 패스스루 정합, roll-forward 항등, 버킷 합=summary 총차기이월, 3축 일관, 연체 집계 | src/composables/__tests__/receivableAging.spec.js |
useForwarding 원천 데이터 (periods, forwarding, total) | src/composables/__tests__/useForwarding.spec.js (존재 시) |
실행: npx vitest run. 수동 확인: 부과 콘솔 → 사이드바 "미수금 연령분석" → 계약/유닛/멤버 탭 전환 → 요약 수치 3축 동일 → 대상명 클릭 차수 펼침 → 차수 버킷 분포 accordion.
10. 알려진 잔여사항
어휘 표준화(D4): 어휘 표준화는 aging 리포트·CarryoverTab·청구상세 컬럼에 적용됨. 수납 콘솔 collecting/{unit,member,contract}/blocks/CriteriaToolBarOrg.vue의 filterScope 토글 라벨("당기분"/"이월분")은 아직 구 어휘 — 후속 정정 대상(Task6 스코프는 컬럼 헤더 한정).