Skip to content

관리비 계약채권 수납 — FE 메인테이너 참고

Wave26 as-built. 실제 관리비의 계약·세대·멤버 3축을 같은 Supabase 수납 원장에 연결한 제품 전용 화면이다.


1. 정적 라우트와 제품 경계

경로소유 제품
/service-charge/actual/console/collecting/contractsrc/views/service-charge/actual/console/collecting/contract/IndexView.vueservice-charge
/service-charge/actual/console/collecting/unitsrc/views/service-charge/actual/console/collecting/unit/IndexView.vueservice-charge
/service-charge/actual/console/collecting/membersrc/views/service-charge/actual/console/collecting/member/IndexView.vueservice-charge

src/router/buildLineRoutes.js의 actual own이 수납 세 segment를 정적으로 제품 전용 view에 연결한다. SERVICE_CHARGE_CORE에는 세 segment가 없으므로 공유 billing _core로 폴백하지 않는다. lease/sales 등 다른 상품의 동일 segment에는 영향을 주지 않는다.

Wave25A 화면과 repo는 service-charge 안에서 닫혀 있다. 회계·임대차 등 다른 제품 컴포넌트나 데이터 repo를 직접 import하지 않는다.


2. 컴포넌트 트리

text
src/views/service-charge/actual/console/collecting/contract/IndexView.vue
├── HeaderOrg.vue
│   ├── useAccountContext
│   ├── useBillingEdition
│   └── ConsoleTabBarOrg(active-group="collecting")
└── MainOrg.vue
    ├── useServiceChargeCollections
    ├── 계약채권 상태 필터·검색·목록
    └── SheetServiceChargeReceivableArt.vue
        ├── read: 금액 요약·채권 정보·수납 이력
        ├── record: 수납 입력 폼
        └── reverse: 정정 사유 폼

세대·멤버 화면은 다음 공용 product-owned subtree를 사용한다.

text
src/views/service-charge/actual/console/collecting/{unit,member}/IndexView.vue
├── projection/HeaderOrg.vue
└── projection/MainOrg.vue
    ├── useServiceChargeProjections(axis)
    ├── 기준일·상태필터·검색·집계표
    └── SheetProjectionArt.vue (exact 계약채권 drilldown, read-only)

상세는 sheet-width-3xl 한 장에서 read/record/reverse mode를 전환한다. 행의 계약명 클릭이 상세 진입이며 별도 “상세” 텍스트 컬럼은 없다.


3. repo/adapter

src/composables/serviceChargeCollectionRepo.js

  • SERVICE_CHARGE_COLLECTION_PRODUCT_KEY = 'service-charge'
  • normalizeServiceChargeReceivable()가 RPC의 snake/camel 필드를 provider-neutral 모델로 정규화
  • 상태가 없을 때만 전달받은 asOfcurrent/overdue/paid 파생
  • makeServiceChargeCollectionRequestKey(command)는 사용자 command마다 새 키 생성
  • DB 오류를 사용자 문구로 매핑하고 provider명은 UI에 노출하지 않음

makeSupabaseServiceChargeCollectionRepo(client) RPC 매핑:

repo 메서드RPC
list({ companyId, officeId, asOf })list_service_charge_receivables_v1
recordReceipt(input)record_service_charge_receipt_v1
reverseReceipt(input)reverse_service_charge_receipt_v1

resolveServiceChargeCollectionRepo()는 production에서 Supabase 설정이 있을 때만 실제 adapter를 만든다. test이거나 설정이 없으면 unavailableServiceChargeCollectionRepo를 반환한다. unavailable은 빈 목록을 샘플 데이터로 위장하지 않고 write를 명시적으로 실패시킨다.

projection read adapter

serviceChargeProjectionRepo.jslist_service_charge_receivable_projections_v1만 호출하고 { projections, unavailableCount, asOf }를 정규화한다. useServiceChargeProjections(axis)는 Company·Office·axis·기준일을 캡처하며 scope/load sequence가 다른 늦은 응답을 폐기한다. 세대·멤버 화면은 mutation을 제공하지 않는다. 수납 기록·정정은 기존 contract repo/command만 사용한다.


4. useServiceChargeCollections

파일: src/composables/useServiceChargeCollections.js

스코프

useAccountContext()에서 다음을 캡처한다.

  • companyId: activeOffice.companyId, 없으면 company.id
  • officeId: activeOffice.id
  • productKey: 항상 service-charge
  • entitlement: activeOffice.modules.includes('service-charge')

Company·Office·Office의 companyId·module 목록이 바뀌면 목록, 선택 행, loading, command/error 상태를 먼저 비우고 새 스코프를 로드한다.

scope-race 방지

세 sequence가 서로 다른 비동기 경로를 막는다.

  • scopeSequence: Company/Office/product 스코프 변경 세대
  • loadSequence: 목록 요청 순서
  • commandSequence: record/reverse 요청 순서

각 요청은 captureScope()로 Company·Office·product·sequence를 보존한다. 응답 시 isCurrentScope()와 해당 sequence가 모두 일치할 때만 상태를 반영한다. 이전 Office의 늦은 list/command 응답은 새 Office 화면에 들어오지 않는다.

record/reverse에는 선택 채권의 현재 balanceAmountexpectedOutstandingAmount로 넘긴다. DB가 revision conflict를 반환하면 command 오류를 보여주고 목록을 다시 읽는다.

청구 확정본 목록도 Office·기간·edition이 바뀌는 즉시 비우고 새 범위로 다시 조회한다. 이전 범위의 지연 응답은 sequence와 scope identity로 폐기한다. 유효 수납 때문에 청구 재오픈 또는 새 버전 확정이 거부되면 useConsoleStage.transitionErrorconfirmationError“먼저 해당 수납을 정정” 안내를 노출하며, 단계 오류는 Office·기간 전환 시 즉시 초기화한다.

부과 확정 payload의 allocation에는 해소된 경우 service-charge-subject.v1 계약·공간·멤버 identity를 함께 동결한다. 실제 v3 청구 builder는 공간 원자의 세금구분 합계·비음수 정수·계약 VAT 합계를 클라이언트에서 먼저 검사하고, 서버는 linked charging Fact와 동일한 subject·공급가액인지 다시 검사한다. 해소되지 않은 운영 차원은 v3 확정을 막되 기획용 v1 화면까지 가짜 UUID로 채우지 않는다.


5. 화면 상태 계약

조건ledgerState/UI
Office/Company 미선택idle, 행 없음
Office에 service-charge 미활성unavailable, 상품 비활성 안내
Supabase 설정/adapter 없음unavailable, 서버 연결 안내
조회 중loading, 로딩 행
연결 성공 + 0건available + connectedEmpty, 확정 계약채권 없음 안내
조회 실패error, 사용자 오류 + 다시 시도
command 실패목록은 유지하고 commandError를 목록/시트에 표시

projection도 idle/loading/available/unavailable/error를 같은 의미로 쓴다. connectedEmpty는 연결 성공 0건만 뜻한다. unavailableCount > 0이면 레거시 코드를 추측 연결하지 않고 “정본 연결이 없는 과거 청구” 경고를 표시한다.

connectedEmpty는 오직 available && receivables.length === 0이다. unavailable의 빈 배열과 실제 연결된 빈 원장을 섞지 않는다.


6. 목록과 상세 UI

목록

  • 상태 필터: current(미수), overdue(연체), paid(완납)
  • 초기 필터: current
  • 현재 v1 RPC에서 실제 채워지는 검색 대상: 계약명·멤버명·부과기간
  • UI normalizer는 향후 응답의 계약 등록코드·공간명·공간 등록코드도 받을 수 있지만, 현재 migration/RPC는 해당 선택 필드를 반환하지 않음
  • 열: 구분, 계약, 멤버, 부과기간, 납기일, 청구액, 수납액, 미수잔액
  • 계약 목록·상세와 세대/멤버 projection 상세는 billingReceivableStatusMeta로 동일한 상태 label·badge를 렌더하며 로컬 statusLabel/statusBadge 맵을 만들지 않음
  • 서버가 반환한 실제 채권만 렌더하며 seed/session fallback은 없음

상세 시트

  • read: 청구/수납/미수 요약, 공급가·세액·기간·납기, 수납 이력
  • record: 정수 양수, 현재 잔액 이하, 오늘 이하 수납일, 방법 필수
  • reverse: 유효 receipt만 진입, 1~500자 사유 필수
  • dirty record/reverse mode에서 취소·닫기는 확인 후 폐기
  • pendingCommand 동안 중복 submit 차단
  • 성공하면 repo가 반환한 한 receivable만 replaceReceivable()로 교체

persistenceEnabled는 product 활성이고 backend가 supabase/test/fake일 때만 true다. unavailable 화면에서는 기록·정정 액션을 노출하지 않는다.


7. 오류 확장 규칙

DB에 새 비즈니스 오류를 추가하면 serviceChargeCollectionErrorMessage()에 사용자 문구를 함께 추가한다. 현재 주요 매핑:

  • 상품/reader/operator 권한
  • receivable/receipt 없음 또는 superseded
  • 청구 재오픈으로 현재 채권에 수납 불가(not_collectible, 안내 후 목록 재조회)
  • 금액 오류·초과 수납
  • 이미 정정됨·정정 사유 누락
  • 수납일·방법·텍스트 길이
  • expected balance 충돌
  • 동일 요청키 payload 충돌

알 수 없는 오류는 provider명을 드러내지 않는 공통 문구로 끝낸다.


8. 테스트 위치

파일대상
src/router/__tests__/buildLineRoutes.spec.jsservice-charge actual 수납 3축이 제품 전용 view로 정적 연결됨
src/composables/__tests__/serviceChargeCollectionRepo.spec.jsnormalize, asOf 상태, 세 RPC argument, unavailable/error mapping
src/composables/__tests__/useServiceChargeCollections.spec.jsCompany·Office·product·expected balance 전달, scope-race 폐기, unavailable/connected-empty 구분
src/composables/__tests__/serviceChargeProjectionRepo.spec.jsprojection RPC·normalizer·unavailable
src/composables/__tests__/useServiceChargeProjections.spec.jsaxis/asOf scope와 late response 폐기
src/components/service-charge/actual/console/collecting/contract/__tests__/ServiceChargeCollectionUi.spec.js계약명 진입, 3xl 상세, connected empty, record/reverse UI·검증
src/composables/__tests__/serviceChargeCollectionLedgerMigration.spec.jsmigration 정적 계약
supabase/tests/service_charge_receivable_receipt_ledger_smoke.sql실제 DB/RLS/RPC/outbox 스모크

9. 다음 확장 포인트

  • 연체료는 overdue 상태 표시에 포함되지 않으며 별도 원장/정책 Wave가 필요하다.
  • reconciliation/notice/external delivery는 이 화면에서 호출하지 않는다.
  • unit/member grain을 추가할 때 subject kind만 억지로 재사용하지 말고, materialization·RPC·표시 식별자 계약을 먼저 확정한다.
  • accounting 소비자는 플랫폼 outbox 이벤트를 구독해야 하며 이 FE에서 회계 repo를 직접 호출하지 않는다.

확장 순서는 DB 계약 → repo adapter → composable scope/state → 제품 전용 page 순서를 유지한다.