Skip to content

FE 메인테이너 — 수납 상세 시트

독자: FE 메인테이너/AI. "이 시트를 이어 개발하려면 무엇을 알아야 하나"를 정의한다. 정본 spec: docs/superpowers/specs/2026-06-19-collecting-detail-sheet-revert-design.md(표현방식 환원) · …/2026-06-19-collecting-detail-page-design.md(원 데이터 모델). BE 계약: docs/handoff/backend/collecting-detail.md.


1. 진입 (라우트 없음 — 시트 오버레이)

  • 전용 페이지/라우트(collecting/:axis/:key)는 제거됨. 상세는 시트 오버레이(SheetCollectingDetailArt, dialog id sheet-collecting-detail-art)로 연다.
  • 진입: collecting 목록(contract|member|unit) 행의 이름 버튼·payment 아이콘 클릭 → goDetail(row):
    js
    selectedCollecting.value = { axis, key: row.key }; // 공유 ref
    document.getElementById("sheet-collecting-detail-art")?.showModal();
  • 시트는 collecting MainOrg(contract/member/unit) 3곳에 전역 1회 마운트. 목록이 언마운트되지 않아 페이지네이션·스크롤 상태가 유지된다(시트화의 핵심 이유).
  • row.key 없는 행(비-canon 라인)은 기존 selectedBilling 폴백(옛 조회 시트) — 무변경.

2. 컴포넌트 트리

components/billing/_core/console/collecting/detail/blocks/SheetCollectingDetailArt.vue   (시트 루트)
├─ sheet-header: "수납 상세" + close
├─ zone-top: 이름+상태배지 / 관리코드 contractCode 우상단
├─ 합계 스트립 (card card-inset-edged: 총청구·원금·연체료·선수금)
├─ CarriedInstallmentsOrg     blocks/  — 이월미납 차수 카드
├─ AllocationStatusOrg        blocks/  — 차수별 충당현황(원금·연체료·수납·잔액) + 차수 ▸ 펼침=원금 과세/면세 세구분(principalByTax)
├─ CollectingActionOrg        blocks/  — ★ 수납처리 액션 패널
│   └─ JournalPreviewMol      blocks/  — 분개 미리보기(인식기준·게이팅)
├─ AdvanceBalanceOrg          blocks/  — 선수금 잔액(자동충당 토글·환급, 2026-07-10부터 환급 확인 다이얼로그 경유 — FB-30)
├─ ReceiptHistoryOrg          blocks/  — 수납 내역(행 펼침 / @cancel emit)
├─ DialogCollectingCancelArt  overlays/ — 수납취소 다이얼로그
└─ sheet-footer: 닫기

블록 위치: src/components/billing/_core/console/collecting/detail/blocks/. 공유 ref: …/console/overlays/selectedCollecting.js.


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

컴포저블/모듈역할
selectedCollecting행→시트 선택 공유 ref {axis,key}
useCollectingDetaildetailFor(axis,key,asOf) — forwarding+lateFee+receipts 조립(§BE 1)
useCollectingAllocationallocate(충당)·orderedTargets(대상행)·allocateToComponent(조정 배분) 순수
useReceiptsrecordReceipt/cancelReceipt/선수금·가수금 원장 (현금 확정 위임)
useReceivableAdjustmentsrecordAdjustment/cancelAdjustment/approveAdjustment (감면·면제·대손 조정 원장)
useLateFee / interestEngine / lateFeePolicy연체료 산출·정책
useModuleSubscriptionaccountingEnabled 게이팅

흐름

SheetCollectingDetailArt
  asOf = ref('2025-12-15')                                  ← selectedCollecting 변경 시 watch로 초기화
  detail = computed(detailFor(selectedCollecting.axis, .key, asOf))  ← asOf 바뀌면 연체료 replay

  ├─ :periods=detail.periods → CollectingActionOrg
  │     amount/periodOrder/componentOrder → allocate() → 충당 결과·수납 결과
  │     paymentDate watch → emit('asOf') ──────────────► onAsOf → asOf 갱신
  │     journalAllocation(computed) → JournalPreviewMol(:recognitionBasis,:accountingEnabled)
  │     emit('confirm', payload{...,discount,lateFeeWaiver,writeOff,reason,periodOrder,componentOrder})
  │        └─► onActionConfirm: 현금→감면→면제→대손 순차
  │              recordReceipt(현금) → useForwarding.collect
  │              recordAdjustment(kind, allocateToComponent(detail.periods,amt,comp,policy))
  │                 → useForwarding.adjust (비현금 미수 차감, 단계마다 fresh detail.periods)

  ├─ AdvanceBalanceOrg(:contract-code,:balance) → useReceipts(환급·자동충당)
  ├─ ReceiptHistoryOrg(:receipts) @cancel → onCancelOpen → DialogCollectingCancelArt
  ├─ ReceivableAdjustmentsOrg(:adjustments=detail.adjustments) @cancel→cancelAdjustment @approve→approveAdjustment
  └─ collect/adjust → useForwarding/useReceipts/useReceivableAdjustments 싱글톤 변이 → detail 재계산
  • 확정 시 감면/대손이 미수를 실제로 차감한다(useForwarding.adjust, collected와 분리). 대손은 승인대기로 생성, 카드에서 승인/취소(가역). 상세는 BE 핸드오프 §6-1.

  • 단방향: 자식(CollectingActionOrg)은 상태를 갖되, 백데이트(asOf)·확정(confirm)은 emit으로 시트에 위임. periods는 항상 prop으로 내려받아 replay 결과를 반영.

  • journalAllocationappliedPrincipal/appliedLateFee computed를 소비(자동=allocate 집계 / 수동=manualRows 합).


4. CollectingActionOrg 내부 상태

ref의미
paymentAmount수납 금액(input[data-amount])
paymentMethod수단(registry Select)
periodOrder / componentOrder충당 축(기본 = lateFeePolicy.collectionPolicy)
isManual / manualRows직접 입력 토글·편집 행(대상 행 미러, 자동값 prefill)
paymentDateCalendarDate, watch→emit('asOf')
discountAmount / lateFeeWaiver / writeOff / reason감면·면제·대손·사유

파생: targetRows(= orderedTargets(periods, policy)금액 무관 차수×구성요소 대상액, due>0만) · autoMap(자동 충당액 조회) · rows(표시: 자동=대상행+충당액 / 수동=manualRows) · appliedPrincipal/appliedLateFee/appliedTotal · excessAmount · reducedDue · shortfallAmount · writeOffAmount · canConfirm(금액>0 또는 조정 존재, !backdateBlocked, 조정 존재 시 reason 필수). confirm payload에 periodOrder/componentOrder 포함.

  • 충당 결과 테이블은 수납 금액 0에서도 targetRows로 대상액을 항상 노출(직접 입력 전제). 충당액은 자동(allocate) 또는 수동 입력. (구 manualLines는 allocated>0만 담아 금액 0이면 빈 표 → 폐기.)
  • 폼 pair 그리드는 grid-cols-1 sm:grid-cols-2 — 모바일에서 셀렉트 풀폭(긴 라벨 드롭다운 삐져나옴 방지).

5. DS 패턴 (CLAUDE.md 준수)

  • 시트 = 유형 A 다중카드, dialog ... sheet sheet-right sheet-width-5xl, sheet-body bg-neutral-subtle. 카드 = bg-neutral-minimal + disclosure disclosure-inset-edged.
  • 합계 스트립은 비접힘 card card-sm card-inset-edged(카드규율3), 가변 리스트는 max-h-* + overflow-y-auto + sticky-thead(카드규율5).
  • 폼: field + field-label + input input-bordered(또는 input-group) — phantom 클래스 금지.
  • 수단·순서 셀렉트 = @/registry/vue/select, 수납일 = @/registry/vue/date-picker.
  • 스위치(직접 입력·대손) = switch switch-md + role="switch" + aria-checked(키보드 합성 click 이중토글 주의 — wrapper @click만).
  • 아이콘 버튼은 button-icon + size + aria-label(ssot B 규칙). 게이트: vitest green · build · ssot B/C/D=0(node scripts/audit-ssot-conformance.mjs .).

6. 게이팅 / 인식기준

  • JournalPreviewMolv-if="accountingEnabled" — 회계 모듈 OFF면 미표시(수납 코어는 유지). localStorage leyve.module.accounting로 토글 시연.
  • recognitionBasislateFeePolicy.recognitionBasis(기본 '발생'). '현금'이면 분개에서 감면/대손 라인 생략(JournalPreviewMol 분기).
  • 연체료 인식 분기(#2, 2026-06-20): 미리보기와 GL 전표(useBillingJournal)가 동일 규칙. 발생주의 = 연체료수익을 발생 시점(E3 vouchersLateFeeAccrual)에 인식 → 수납 미리보기는 연체료분을 미수관리비(연체료분)으로 표기(수익 재인식 아님·이중계상 방지). 현금주의 = E3 없음 → 수납 시점에 연체료수입(0922)으로 split. 정본 규칙: BE handoff collecting-detail.md §4.

7. 확장 포인트

  • 상세 딥링크: 시트는 URL을 안 바꾼다. 상세 URL 공유가 필요하면 selectedCollecting를 쿼리(?detail=contract:c-hong)와 동기화하는 라우트 가드를 추가(현재 범위 밖).
  • 원금 과세/면세/영세 다중 분해: allocate의 component를 ['principal']에서 세구분 배열로 확장 → JournalPreviewMol VAT 분해를 라인별 taxCategory로(현재 과세 가정 /1.1).
  • 대손 승인 실연계: 현재 approveAdjustment는 상태 토글 스텁. 방금 머지된 전자결재(approval) 모듈과 연계 + 대손충당금 잔액 추적/부족분 대손상각비(0835) 분기(현재 전액 충당금 상계 가정)는 후속.
  • 충당현황 원청구↔잔액 구분: AllocationStatusOrg 원금이 잔여(outstanding) 기준 — 원청구 표기 정정은 후속(범위 밖).
  • 원금 taxCategory 정밀화: A(소계 배관) + B 완료(충당 세구분). principalByTaxderivePrincipalByTax(과세→면세→영세 greedy)로 부분수납에서도 정밀(비례 근사 폐기), allocatecomponentOrder 배열로 과세원금/면세원금/영세원금/연체료 4구분 충당, 충당현황↔패널 일치. lateFeeWaiver는 forwarding 미차감(연체료 미추적). 고급: 저장형 per-구분 원자·연체료 정식 원자·끝수정책·componentOrder 커스텀 UI·다축 fan-out. C(분개 VAT taxCategory별)·D(증빙)는 후속. 정본 …/specs/2026-06-20-principal-allocation-by-tax-design.md.
  • componentOrder 배열: CollectingActionOrg가 UI 토글(원금우선/연체료우선)을 COMPONENT_ORDER_ARRAY로 매핑해 policy/confirm payload에 배열 전달. allocate/orderedTargets/allocateToComponent는 배열·문자열 양립.
  • 타 라인 상세 시트화: 현재 시트는 key 있는 service-charge actual 행만. 옛 mock 시트(SheetReadBillingArt/SheetCollectingUpdateArt) 통합은 후속.

8G. G1 — 성격×세금 구성요소 일반화 (byComponent)

G1이 B서브프로젝트의 "세금만" 모델을 성격(nature)×세금 통합 모델로 확장한 내용을 정리한다.

데이터 흐름 — byComponent 경로

useAllocations.lineTotalsByComponent()
  → componentOf(charge)로 각 라인을 구성요소 키에 합산
  → invoicingByContract/Unit/Member: currentPeriodByComponent 포함
  → useForwarding.buildRow({ currentByComponent })
       → period별 deriveByComponent(billedByComponent, collected+adjusted)
       → period.byComponent (Σ === outstanding)
       → period.principalByTax 파생 뷰 (B 호환: byComponent의 매출 서브셋)
  → useCollectingDetail.detailFor → detail.periods[].byComponent
  → AllocationStatusOrg props.periods[].byComponent
       → taxParts(p): value>0 필터 → COMPONENT_LABEL 라벨 변환 → 펼침 행 표시

AllocationStatusOrg — 구성요소 펼침 일반화

AllocationStatusOrg.vue (블록 위치 …/collecting/detail/blocks/):

js
// 구성요소 라벨 맵 — 표시 이름 단일 출처
const COMPONENT_LABEL = {
  과세원금: "과세 원금",
  면세원금: "면세 원금",
  영세원금: "영세 원금",
  장기수선충당금: "장기수선충당금",
  예비비적립금: "예비비적립금",
};
// 차수 펼침 행 — byComponent 우선, 없으면 principalByTax 폴백(B 레거시 호환)
const taxParts = (p) => {
  const bc =
    p.byComponent ||
    (p.principalByTax
      ? {
          과세원금: p.principalByTax.과세,
          면세원금: p.principalByTax.면세,
          영세원금: p.principalByTax.영세,
        }
      : {});
  return Object.entries(bc)
    .filter(([, v]) => v > 0)
    .map(([k, v]) => [COMPONENT_LABEL[k] ?? k, v]);
};

핵심 동작:

  • value > 0 필터: 0인 구성요소는 행에 표시하지 않음. 데모처럼 영세 0이면 숨김.
  • byComponent 우선 / principalByTax 폴백: 기존 B 소비처(principalByTax만 있는 period)와 호환.
  • COMPONENT_LABEL: 라벨 표시 이름의 단일 출처. 신규 구성요소 추가 시 이 맵에만 추가.

principalByTax 파생 호환

useForwardingperiod.principalByTaxbyComponent에서 매출 서브셋을 뽑는 파생 뷰:

js
principalByTax: { 과세: bc.과세원금 || 0, 면세: bc.면세원금 || 0, 영세: bc.영세원금 || 0 }

B 서브프로젝트 소비처(충당 세구분 B — derivePrincipalByTax, CollectingActionOrg 과세→면세→영세 greedy)는 무변경. byComponent가 더 넓은 집합이고 principalByTax는 항상 파생된다.

G2~G4 확장 포인트

서브프로젝트FE 확장 포인트
G2CollectingActionOrg 충당 결과 행에 장기수선충당금 등 부채/자본 구성요소 행 추가 (orderedTargets byComponent 지원)
G3JournalPreviewMolvouchersForReceipt에서 대변 계정을 성격별(creditAccountName)로 분기 + 과세/면세 VAT 분해(현재 전액 과세 /1.1 가정)
G4componentOrder 프리셋 셀렉트 UI (DEFAULT_COMPONENT_ORDER 재정렬, 설정 저장)

8. 테스트 위치

대상테스트
allocate·orderedTargets·allocateToComponent 순수src/composables/__tests__/useCollectingAllocation.spec.js
useForwarding adjust/unadjust(미수 비현금 차감)src/composables/__tests__/useForwarding.spec.js
조정 원장(record/cancel/approve·미수 차감)src/composables/__tests__/useReceivableAdjustments.spec.js
조정 전표(대손/에누리/면제 균형)src/composables/__tests__/useBillingJournal.spec.js
조정 내역 카드(배지·상태·취소/승인)…/detail/blocks/__tests__/ReceivableAdjustmentsOrg.spec.js
액션 패널(충당·확정·대손·감면·reason 필수)…/detail/blocks/__tests__/CollectingActionOrg.spec.js
분개 미리보기(균형·게이팅·인식기준)…/detail/blocks/__tests__/JournalPreviewMol.spec.js
상세 시트(렌더·replay·확정·대손→미수=0+승인대기)…/detail/blocks/__tests__/SheetCollectingDetailArt.spec.js
진입점(행 클릭 → selectedCollecting+showModal)…/collecting/{contract,member,unit}/blocks/__tests__/DynamicTableOrg.spec.js
원금 taxCategory 소계(A)useAllocations.spec.js(currentPeriodByTax) · useCollectingDetail.spec.js(표면화) · AllocationStatusOrg.spec.js(행펼침)
충당 세구분(B)useCollectingAllocation.spec.js(derivePrincipalByTax·배열 allocate 4구분) · useForwarding.spec.js(부분수납 정밀) · CollectingActionOrg.spec.js(세구분 행) · useReceivableAdjustments.spec.js(lateFeeWaiver 미차감)
성격×세금 레지스트리(G1)src/composables/__tests__/billingNature.spec.js (componentOf·natureGroup·isVatable · 부채/자본 charge → nature키 · 매출 폴백)
byComponent 일반화(G1)useAllocations.spec.js(currentPeriodByComponent Σ===currentPeriod) · useForwarding.spec.js(byComponent Σ===outstanding · principalByTax 파생) · AllocationStatusOrg.spec.js(장기수선충당금 포함 전 구성요소 표시 · value>0 필터)

실행: npx vitest run. Playwright 시나리오(수동): 행 클릭→시트 오버레이(URL 불변)·금액→충당·수납일→연체료·대손+사유→확정→미수 차감·조정 내역 카드 승인대기→승인/취소 원복·회계모듈 ON/OFF·시트 닫기→목록/페이지 유지, console 0.

충당순서 배선 (G2, 2026-06-20)

  • 모듈: src/composables/allocationOrder.jscompileOrder(preset) · ALLOCATION_PRESETS · DEFAULT_ALLOCATION_PRESET='연체료우선'. 원금 블록은 billingNature.DEFAULT_COMPONENT_ORDER 재사용.
  • 소비: CollectingActionOrg.vuecomponentOrderArray = computed(() => compileOrder(componentOrder.value)) 로 컴파일 → orderedTargets/allocate(useCollectingAllocation, 인터페이스 무변경)에 배열로 전달. 부채/자본 행은 dueOf(p, comp)=p.byComponent[comp]로 자동 생성.
  • 합계 분류: manual 모드는 LATE_FEE_COMPONENTS 보집합으로 appliedPrincipal(비연체료)/appliedLateFee 산출. auto 모드는 allocate() 결과 소비.
  • 충당현황 일치: AllocationStatusOrgbyComponent 펼침은 deriveByComponent(DEFAULT_COMPONENT_ORDER 순)와 동일 원금 블록 순서 → 패널과 정렬 일치.
  • 분개 미접촉: journalAllocation/JournalPreviewMol은 flat 합계 유지(G3에서 성격별 대변·과세 VAT 분리).
  • 테스트: __tests__/allocationOrder.spec.js, useCollectingAllocation.spec.js(분류), CollectingActionOrg.spec.js(부채 행 노출·디폴트 순서).
  • G4 확장점: 충당순서 계층 셀렉터 UI(축2~4). compileOrder에 구조화 프리셋 입력 추가.

부가세 표시 (G3a, 2026-06-20)

  • VAT 주입 지점 = useForwarding.buildRow(withVat). useAllocations·세금계산서 무편집.
  • period.byComponent.부가세(과세원금 직후) → AllocationStatusOrg·CollectingActionOrg COMPONENT_LABEL부가세:'부가세' → 충당현황 펼침·패널 충당행에 부가세 노출.
  • 미수/청구/totalDue = 공급대가(부가세 포함). compileOrder는 DEFAULT_COMPONENT_ORDER(부가세 포함) 자동 소비.
  • 테스트: billingNature.spec(VAT_RATE·withVat·순서), useForwarding.spec(공급대가·36,625), CollectingActionOrg.spec(부가세 행).
  • G3b: 분개 성격별 대변+부가세예수금. G4: 충당순서 계층 UI.

G3b — 청구 분개 성격별 대변 (2026-06-20)

  • 활성 프리셋 상태: useBillingPreset()(싱글톤, localStorage leyve.billing.preset). { activePreset, setPreset, clearPreset, componentCredit }. 데모 기본 = 공동주택.
  • 분개 소비처: useBillingJournal.vouchersInvoicing()componentCredit()(범용 ∪ 활성 프리셋 도메인 매핑)을 읽어 성격별 대변 라인을 생성. 라인 수는 가변(존재하는 성격만, 과세 VAT 존재 시 0255 추가).
  • nameOf 폴백: core(0xxx) 미발견 시 SERVICE_CHARGE_ACCOUNTS nameKo 폴백 → 9112='장기수선충당금', 9113='수선적립금', 9201='예비비적립금'.
  • JournalPreviewMol: E4(수납) 소비 — 본 변경(E2) 무관, 무수정.
  • 확장 포인트: 프리셋 선택 UI(현재 코드/localStorage로만 전환)·6뷰 activePacks 연동은 후속.