다크모드
기초데이터 — 개시 미수 마스터 (BE 참고 매뉴얼)
관리비 단독 상용화 프로그램 #3 (P0) — 2026-07-01 완료. 정본 설계 spec:
docs/superpowers/specs/2026-07-01-initial-data-onboarding-design.md연관 FE 핸드오프:docs/handoff/frontend/service-charge-initial-data.md⚠ 2026-07-06 W3b 마스터 단일화 갱신 — Fable 1차 감사 FB-13(축 미러 stale)·FB-4(오버레이 비대칭)·FB-19(당기 라벨 이중차감) 해소..unit/.member는 더 이상 별도 저장 미러가 아니라.contract+priorIdentity(정적 사실맵)에서 매번 파생한다. 설계:docs/superpowers/specs/2026-07-06-initial-receivables-master-unify-design.md. 아래 §2-2·§5·§7·§8이 신 as-built로 갱신됨(구 서술은 취소선/삭제).
1. 기능 개요
신규 단지 온보딩 시 이전부터 존재하던 미납 관리비(개시 미수)를 세대별·차수별로 입력한다. 입력된 데이터는 캐논 사슬을 통해 이월/미수 파생 → 연체료 → 미수금 연령분석 → 고지서 전기이월 → 개시 분개(E1)까지 자동 전파된다.
현재 구현은 클라이언트 사이드 모의 데이터(reactive singleton)다. 실 서비스에서는 이 단원이 기술하는 데이터 모델과 API 계약을 백엔드가 제공해야 한다.
2. 데이터 모델
2-1. 마스터 그레인
진실원 단위 = (계약, 차수).
데이터 모델 표기:
priorPeriods는 VueRef<{ contract, unit, member }>객체이므로, 실제 코드에서 계약 맵에 접근할 때는priorPeriods.value.contract를 사용한다. 아래 의사코드에서는 가독성을 위해.value를 생략한다.
priorPeriods.value.contract = {
'<contractCode>': [
{
label: 'YYYY-MM', // 차수 (필수)
dueDay: 5, // 납기일(일자, 선택 — 미지정 시 DEFAULT_DUE_DAY=5)
billedByComponent: {
과세원금: 4000, // 공급가액 기준, excl. VAT (없으면 0 — 제로 기본값)
면세원금: 6000, // 공급가액 기준 (없으면 0 — 제로 기본값)
영세원금: 0 // (없으면 0 — 제로 기본값)
}
},
...
],
...
}billedByComponent 키 필수/선택 정정: 세 키(과세원금·면세원금·영세원금) 모두 의미상 선택적-제로-기본값이다.
importCsv는 항상 세 키를 0으로 초기화하며, 데모 시드(demoSeed)는 영세원금 키를 생략할 수 있다. "필수"/"선택" 구분보다 정확한 표현은 "누락 시 0으로 처리"이다. 어떤 키도 하드 필수값을 요구하지 않는다.
핵심 원칙:
- 저장 값 = 공급가액(excl. VAT). 공급대가(VAT 포함)는
withVat()함수가 파생한다. - 연체료는 저장하지 않는다 —
dueDay+ 차수 경과 일수 + 단계이율 정책(lateFeePolicy)이 파생한다. label=YYYY-MM정규식/^\d{4}-(0[1-9]|1[0-2])$/검증.
2-2. 3축 투영 (2026-07-06 W3b 갱신 — 파생 아키텍처)
priorPeriods는 { contract, unit, member } 3-맵을 노출하지만, 저장소는 contract 하나뿐이다:
.contract= 유일 진실원(contractPeriodsref). CRUD(addPeriod/updatePeriod/removePeriod/upsertContract/removeContract) 전부 이 맵만 변경한다..unit/.member= 매 접근 시 computed 파생(unitPeriods/memberPeriodsinuseInitialReceivables.js). 저장소가 아니므로 stale이 구조적으로 불가능하다(FB-13 근본 해소).- 파생 경로: 정적
priorIdentity(contract→{unitCode, memberCode}, 시드에서 1회 도출·CRUD로 불변)로.contract의 각 계약을 unitCode/memberCode 그룹으로 묶고, 같은 축에 매핑된 여러 계약의 차수를 label 기준 병합(mergePriorArrays— billedByComponent 성분별 합산, dueDay는 최초 정의값 유지)한다. - 데모는 1계약=1유닛=1멤버(1:1)이라 사실상 통과합산이지만, 다계약→1유닛(세대분할·합가) 케이스도 일반화되어 옳게 동작한다.
priorIdentity에 없는 계약(신규 생성된 테스트 계약 등)은 축 파생 대상에서 제외 —.contract만 진실원 유지.
- 파생 경로: 정적
- 신규 입력(계약 기준)은 여전히
contract맵에만 쓴다..unit/.member·당기 부과 있는 계약의 forwarding 집계·useForwarding의 prior-only 폴백 — 전부 자동 반영(파생 체이닝, 수동 미러 갱신 불요). - prior-only 세대 신규 생성(새 unitCode/memberCode를 갖는 계약을 처음부터 만드는 것)은 여전히 현 스코프 밖 — 단,
priorIdentity에 등재된 기존 prior-only 계약(c-end등) 편집은 축에 정상 반영된다(이 부분이 W3b 이전엔 안 됐음 — FB-13 증상).
삭제된 구서술(회귀 방지 기록): "시드 초기화 시 전체 3-맵을 deep clone으로 적재" — 더는 사실이 아니다. deep clone은
.contract에만 적용되고(seedContractPeriods),.unit/.member는 위 파생 경로로만 존재한다.
3. 캐논 사슬
priorPeriods (개시 미수 마스터 — useInitialReceivables)
→ useForwarding (이월/미수 차수별 FIFO 충당, 3축)
→ useReceivableAging (미수금 연령분석 — 차수 버킷)
→ interestEngine / lateFeePolicy (연체료 차수별 단계이율)
→ InvoiceDynamicOrg (고지서 전기이월 표시)
→ useBillingJournal.vouchersOpeningReceivable (E1 개시분개)useForwarding이 priorPeriods를 reactive getter로 소비하므로 마스터 변경 → forwarding computed 재계산 → 하위 전부 자동 전파.
4. E1 개시 분개 규칙
개시 미수는 개시 시점 회계 분개(E1)를 생성한다:
| 구분 | 계정과목 | 코드 | 비고 |
|---|---|---|---|
| DR | 미수금 | 0108 | 공급대가(VAT 포함) |
| CR | 이월이익잉여금 | 0375 | 과세·면세·영세 원금 합계 |
| CR | 부가세예수금 | 0255 | 과세원금 × 10% |
- 과세·면세·영세 구분 각각 별도 라인으로 분개.
- 연체료는 E1에 포함하지 않는다(별도 발생 분개).
- 구현:
useBillingJournal.js→vouchersOpeningReceivable().
5. 불변식
| 불변식 | 내용 |
|---|---|
| 시드 회귀0 | 모듈 초기화 시 buildSeed(DEMO_CONFIG).priorPeriods.contract deep clone(.unit/.member는 파생이라 clone 대상 아님). 시드 원본 객체 불변 보장. |
| 파생==오라클 회귀0 (W3b) | 시드 상태에서 파생 unitPeriods/memberPeriods 값이 buildSeed().priorPeriods의 구 .unit/.member 리터럴(현재는 회귀0 대조 오라클로만 유지, 편집 소스 아님)과 값 동일이어야 한다. 다르면 파생 로직(priorIdentity 매핑 or mergePriorArrays) 결함 — STOP. |
| 축 미러 stale 불가 (FB-13, W3b) | .unit/.member가 별도 저장소가 아니므로, .contract 편집(addPeriod/updatePeriod/removePeriod/removeContract) 즉시 두 축에 반영된다. "계약축만 갱신되고 유닛/멤버축은 그대로"인 상태는 구조적으로 발생할 수 없다. |
| 당기 라벨 거부 (FB-19, W3b) | addPeriod/importCsv가 label === CURRENT_PERIOD이면 거부({ ok:false, reason } or CSV 오류행) — 개시미수 마스터는 이월(prior) 전용, 당기는 캐논(useAllocations)에서 파생되므로 같은 label로 이중 존재하면 useForwarding paidMap 키 공유로 수납이 이중차감된다. |
| 오버레이 purge 의무 (FB-4, W3b) | 소비처(SheetCuOpeningArt 저장 흐름)가 removePeriod(cc, label) 호출 시 반드시 useForwarding.purgeOverlay('contract', cc, label)을 함께 호출한다. 안 하면 고아 collected/adjusted가 남아 동일 label 재등록 시 유령 수납으로 부활한다. useInitialReceivables는 useForwarding을 import하지 않으므로(순환 금지) 마스터 쪽이 자동으로 purge하지 못한다 — 이 계약은 코드 리뷰로 강제해야 한다. |
| 초과수납 무음 증발 금지 (FB-4, W3b) | updatePeriod로 billed를 기수납(paid+adj) 미만으로 낮추면 outstanding은 여전히 max(0,...) 클램프(음수 미수 없음)이나, useForwarding.overpaidWarnings()로 초과분을 조회 가능해야 한다. periodStatus의 '완납' 어휘 자체는 변경하지 않았다(status 확대는 blast radius가 커 후속 — 이번 웨이브는 경고 노출까지). |
| 기존 테스트 전통과 | useForwarding.spec.js · forwardingCarryoverDisplay.spec.js · receivableAging.spec.js · useFinancialStatements.spec.js(E1) 전부 시드 직후 출력과 동일 — 회귀 0건 기대. |
| 3-맵 구조 보존 | priorPeriods.value의 { contract, unit, member } 최상위 키 구조를 삭제하거나 재구성하지 않는다(내부 구현이 저장/파생으로 나뉘어도 표면 표기는 무변경). |
6. CSV 계약 (import API)
6-1. 헤더
계약코드,입주민,차수,납기일,과세원금,면세원금,영세원금입주민열은 파싱 시 무시(참고용).- 첫 줄에
계약코드가 포함되면 헤더로 간주하고 스킵.
6-2. 행 검증 (per-row)
| 조건 | 에러 사유 |
|---|---|
계약코드 열이 비어 있음 | '계약코드 누락' |
차수 열이 YYYY-MM 형식 불일치 | '차수 형식 오류(YYYY-MM)' |
- 검증 실패 행은 건너뛰고
errors배열에{ line, reason }추가. line= 1-indexed(헤더 포함). 헤더가 1번이면 첫 데이터 행은 2번.- 부분 성공 허용 — 성공한 행은 즉시
addPeriod처리.
6-3. 숫자 파싱
- 따옴표 인지(quote-aware) CSV:
"4,000"→ 4000. - 빈값·파싱 실패 → 0으로 처리(에러 없음).
6-4. upsert 동작
같은 (계약코드, 차수) 조합이 이미 있으면 덮어씌운다. 신규이면 추가.
7. API 요약 (composable — 실 BE 계약 참조용)
| 메서드 | 시그니처 | 설명 |
|---|---|---|
addPeriod | (contractCode, { label, dueDay?, billedByComponent }) | 차수 추가(같은 label이면 upsert) |
updatePeriod | (contractCode, label, patch) | 특정 차수 partial update |
removePeriod | (contractCode, label) | 차수 삭제 |
upsertContract | ({ contractCode }) | 계약(세대) 진입 보장 — 차수 배열 없으면 빈 배열 생성 |
removeContract | (contractCode) | 계약 전체 삭제 |
importCsv | (text) → { added, errors: [{ line, reason }] } | CSV 텍스트 일괄 파싱·import. label===CURRENT_PERIOD 행은 errors로 분리(W3b). |
resetToSeed | () | 시드 상태로 초기화(테스트·데모) |
cross-composable (useForwarding — W3b 신설, useInitialReceivables가 아니라 useForwarding이 소유):
| 메서드 | 시그니처 | 설명 |
|---|---|---|
purgeOverlay | (axis, key, label) | 해당 축/키/label의 collected/adjusted 오버레이 삭제. removePeriod와 반드시 동반 호출(FB-4). |
overpaidWarnings | () → [{ contractCode, name, label, billed, settled, overpaid }] | contract 축을 스캔해 settled(=collected+adjusted) > billed인 차수를 경고로 노출. |
8. 엣지 케이스
| 케이스 | 현재 처리 |
|---|---|
prior-only 세대(부과종료·일시중단) — PRIOR_PERIODS.unit/.member 폴백 | 시드가 보유한 prior-only 세대(c-end, c-gap 등)는 unit/member 파생(W3b — priorIdentity 매핑, 더 이상 고정 미러 아님)으로 forwarding 처리. 이 계약을 편집(addPeriod/updatePeriod/removePeriod)하면 파생이 즉시 반영된다(FB-13 이전엔 stale이었음). 화면에서 축 정체성이 없는 신규 계약을 처음부터 생성하는 케이스는 여전히 스코프 밖(#3 미포함). |
| 선수금·가수금 개시 잔액 | 현 스코프 밖 — 미수만 처리. 별도 후속 작업. |
| 연체료 컬럼 | 현재 테이블에 미표시(interestEngine 통합 후 추가 예정). |
| 동일 계약코드 중복 import | upsert — 후입 값으로 덮어씌움. |
| 납기일 미지정 | DEFAULT_DUE_DAY = 5 적용. |
| 차수 삭제 후 동일 label 재등록 (W3b) | removePeriod 단독 호출은 오버레이(collected/adjusted)를 고아로 남긴다 — 소비처가 purgeOverlay 동반 호출해야 유령 수납 부활을 막는다(§5 "오버레이 purge 의무"). |
| billed 하향이 기수납보다 작음 (W3b) | outstanding은 max(0,...) 클램프되어 음수가 되지 않지만, overpaidWarnings()로 조회 가능한 경고를 남긴다. periodStatus 어휘(예: '완납') 자체는 정정하지 않는다 — 표시층(SheetCuOpeningArt)의 경고 배지로만 가시화. |
| 당기(CURRENT_PERIOD) label 등록 시도 (W3b) | addPeriod 거부({ ok:false, reason })·importCsv는 해당 행을 errors로 분리. 개시미수 마스터는 이월 전용. |
9. 테스트 위치
| 파일 | 커버 범위 |
|---|---|
src/composables/__tests__/useInitialReceivables.spec.js | 시드 초기화 동등성, CRUD, importCsv 파싱, forwarding 재배선 동등성, 입력→전파 검증. W3b 추가: 당기 label 거부(addPeriod/importCsv) |
src/composables/__tests__/useForwarding.spec.js | forwarding 재배선 후 출력 동등성. W3b 추가: addPeriod stale 미반영→반영 확인(FB-13 RED→GREEN), removeContract 3축 소멸, 파생==contract 직접대조, purgeOverlay 유령수납 부활 없음, overpaidWarnings 노출 |
src/components/service-charge/setting/initial-data/general/overlays/__tests__/SheetCuOpeningArt.spec.js | W3b 신규: 정상 상태 무경고 / billed 하향 시 초과수납 배지+툴팁 노출(mount) |
src/composables/__tests__/receivableAging.spec.js | 미수금 연령분석 downstream 회귀 |
src/composables/__tests__/useFinancialStatements.spec.js | E1 개시분개 회귀 |