다크모드
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 idsheet-collecting-detail-art)로 연다. - 진입: collecting 목록(
contract|member|unit) 행의 이름 버튼·payment 아이콘 클릭 →goDetail(row):jsselectedCollecting.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} |
useCollectingDetail | detailFor(axis,key,asOf) — forwarding+lateFee+receipts 조립(§BE 1) |
useCollectingAllocation | allocate(충당)·orderedTargets(대상행)·allocateToComponent(조정 배분) 순수 |
useReceipts | recordReceipt/cancelReceipt/선수금·가수금 원장 (현금 확정 위임) |
useReceivableAdjustments | recordAdjustment/cancelAdjustment/approveAdjustment (감면·면제·대손 조정 원장) |
useLateFee / interestEngine / lateFeePolicy | 연체료 산출·정책 |
useModuleSubscription | accountingEnabled 게이팅 |
흐름
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 결과를 반영.
journalAllocation은appliedPrincipal/appliedLateFeecomputed를 소비(자동=allocate 집계 / 수동=manualRows 합).
4. CollectingActionOrg 내부 상태
| ref | 의미 |
|---|---|
paymentAmount | 수납 금액(input[data-amount]) |
paymentMethod | 수단(registry Select) |
periodOrder / componentOrder | 충당 축(기본 = lateFeePolicy.collectionPolicy) |
isManual / manualRows | 직접 입력 토글·편집 행(대상 행 미러, 자동값 prefill) |
paymentDate | CalendarDate, 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-bodybg-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. 게이팅 / 인식기준
JournalPreviewMol은v-if="accountingEnabled"— 회계 모듈 OFF면 미표시(수납 코어는 유지). localStorageleyve.module.accounting로 토글 시연.recognitionBasis는lateFeePolicy.recognitionBasis(기본 '발생'). '현금'이면 분개에서 감면/대손 라인 생략(JournalPreviewMol 분기).- 연체료 인식 분기(#2, 2026-06-20): 미리보기와 GL 전표(
useBillingJournal)가 동일 규칙. 발생주의 = 연체료수익을 발생 시점(E3vouchersLateFeeAccrual)에 인식 → 수납 미리보기는 연체료분을미수관리비(연체료분)으로 표기(수익 재인식 아님·이중계상 방지). 현금주의 = E3 없음 → 수납 시점에연체료수입(0922)으로 split. 정본 규칙: BE handoffcollecting-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 완료(충당 세구분).
principalByTax는derivePrincipalByTax(과세→면세→영세 greedy)로 부분수납에서도 정밀(비례 근사 폐기),allocate는componentOrder배열로 과세원금/면세원금/영세원금/연체료 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 파생 호환
useForwarding의 period.principalByTax는 byComponent에서 매출 서브셋을 뽑는 파생 뷰:
js
principalByTax: { 과세: bc.과세원금 || 0, 면세: bc.면세원금 || 0, 영세: bc.영세원금 || 0 }B 서브프로젝트 소비처(충당 세구분 B — derivePrincipalByTax, CollectingActionOrg 과세→면세→영세 greedy)는 무변경. byComponent가 더 넓은 집합이고 principalByTax는 항상 파생된다.
G2~G4 확장 포인트
| 서브프로젝트 | FE 확장 포인트 |
|---|---|
| G2 | CollectingActionOrg 충당 결과 행에 장기수선충당금 등 부채/자본 구성요소 행 추가 (orderedTargets byComponent 지원) |
| G3 | JournalPreviewMol — vouchersForReceipt에서 대변 계정을 성격별(creditAccountName)로 분기 + 과세/면세 VAT 분해(현재 전액 과세 /1.1 가정) |
| G4 | componentOrder 프리셋 셀렉트 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.js—compileOrder(preset)·ALLOCATION_PRESETS·DEFAULT_ALLOCATION_PRESET='연체료우선'. 원금 블록은billingNature.DEFAULT_COMPONENT_ORDER재사용. - 소비:
CollectingActionOrg.vue가componentOrderArray = computed(() => compileOrder(componentOrder.value))로 컴파일 →orderedTargets/allocate(useCollectingAllocation, 인터페이스 무변경)에 배열로 전달. 부채/자본 행은dueOf(p, comp)=p.byComponent[comp]로 자동 생성. - 합계 분류: manual 모드는
LATE_FEE_COMPONENTS보집합으로appliedPrincipal(비연체료)/appliedLateFee산출. auto 모드는allocate()결과 소비. - 충당현황 일치:
AllocationStatusOrg의byComponent펼침은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·CollectingActionOrgCOMPONENT_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()(싱글톤, localStorageleyve.billing.preset).{ activePreset, setPreset, clearPreset, componentCredit }. 데모 기본 = 공동주택. - 분개 소비처:
useBillingJournal.vouchersInvoicing()가componentCredit()(범용 ∪ 활성 프리셋 도메인 매핑)을 읽어 성격별 대변 라인을 생성. 라인 수는 가변(존재하는 성격만, 과세 VAT 존재 시 0255 추가). - nameOf 폴백: core(0xxx) 미발견 시
SERVICE_CHARGE_ACCOUNTSnameKo 폴백 → 9112='장기수선충당금', 9113='수선적립금', 9201='예비비적립금'. - JournalPreviewMol: E4(수납) 소비 — 본 변경(E2) 무관, 무수정.
- 확장 포인트: 프리셋 선택 UI(현재 코드/localStorage로만 전환)·6뷰 activePacks 연동은 후속.