Skip to content

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.vueMainOrg를 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이벤트
MainOrgMainOrg.vue
HeaderOrgHeaderOrg.vueaxis: Stringupdate:axis
SummaryStripMolblocks/SummaryStripMol.vuesummary: Object
AgingTableOrgblocks/AgingTableOrg.vuerows: Array
BucketStripOrgblocks/BucketStripOrg.vuebuckets: Array

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

컴포저블 체인

useReceivableAging()                 (src/composables/useReceivableAging.js)
  └─ useForwarding()                 (내부: forwardingByContract/Unit/Member, isAnomalous)

useReceivableAginguseForwarding 위의 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의 탭 클릭 → MainOrgaxis 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로 강조. 나머지는 숫자만.

차수별 미수 분포

BucketStripOrgdisclosure 컴포넌트(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-subtle td + 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 자체는 useForwarding API만 소비하므로 변경 없음 또는 최소.
  • 드릴다운 배선: 대상명 클릭 → 수납 콘솔 해당 행 또는 계정 허브 시트로 연결. 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 스코프는 컬럼 헤더 한정).