Skip to content

관리비(service-charge) 커밋 레이어 — BE 핸드오프

독자: BE 개발자/AI. 부과 편집 스냅샷과 단계 진행 상태는 서로 다른 Supabase 계약으로 Office·정산회차별 영속한다. 이 문서는 현재 구현과 최종 부과·분개 트랜잭션으로 이어받을 경계를 정의한다. 근거: docs/AUDIT-SERVICE-CHARGE-DEMO-READINESS-2026-06-27.md와 Wave 11 확정 UI·repo 구현. 현재 부과 확정은 불변 snapshot과 단계 상태를 단일 RPC로 서버에 기록한다.

Wave 11 as-built — 상품 소유 확정 원장과 선택 연결

정본 migration은 20260717185616_service_charge_confirmation_facts.sql이다.

  • service_charge_charging_confirmations: 부과 금액·배분·계산 버전의 immutable snapshot
  • service_charge_invoice_versions: 부과 확정을 참조하는 문서별 청구 immutable snapshot
  • confirm_service_charge_facts(...): Office 권한, 단계, expected workflow revision, 전체 payload, 0원·미배분을 검증하고 사실·단계 전이·플랫폼 이벤트를 한 트랜잭션으로 기록
  • 공통 이벤트: service_charge.charging_confirmed, service_charge.invoice_version_confirmed
  • 회계를 구독한 Office만 accounting-source-event-v1, 전자문서를 구독한 Office만 electronic-document-source-v1 delivery가 생긴다.

핵심 불변식은 source fact first다. 대상 상품의 테이블·RPC·FK를 참조하지 않는다. 선택 delivery 예약은 예외 블록으로 격리되어 실패해도 관리비 사실과 플랫폼 이벤트를 롤백하지 않는다. exact request retry는 같은 응답을 반환하고 payload가 달라지면 conflict다. 공개 테이블은 SELECT만 허용하며 update/delete trigger가 확정 snapshot 변경을 막는다.

SQL 검증은 supabase/tests/service_charge_confirmation_facts_smoke.sqlservice_charge_invoice_charging_total_smoke.sql, 구조 검증은 serviceChargeConfirmationFactsMigration.spec.js다. FE는 부과의 serviceChargeConfirmationSnapshot.js, 청구의 serviceChargeInvoiceSnapshot.js, 공통 serviceChargeConfirmationRepo.js를 통해 이 계약을 소비한다.

Wave 16 as-built — 정기 관리비 청구 Fact UI

  • actual 청구 계약·유닛·멤버 ActionBar는 useInvoicingCommit으로 같은 confirm_service_charge_facts(..., p_stage='invoicing')를 호출한다. 다른 billing line과 provisional은 기존 공유 stage 동작을 유지한다.
  • 확정 직전 최신 service_charge_charging_confirmations를 읽고, 현재 계약별 청구 draft를 service-charge-invoice.v1으로 고정한다. sourceChargingConfirmation.factId/factVersion/workflowRevision이 원본 부과 Fact를 가리킨다.
  • 금액 계약은 invoice.snapshot.supplyAmount = invoicingByContract.currentPeriod = charging confirmation totals.allocated다. 즉 부과 배분 원금은 공급가액이고, 부가세는 계약의 과세원금 × VAT_RATE를 반올림해 별도로 더한다. 세 공급가액 합이 부과 확정의 allocated와 다르면 FE가 hard-stop한다. 서버는 invoice 내부의 totalAmount = supplyAmount + taxAmount뿐 아니라 아래 linked Fact 등식을 별도 trigger로 재검증한다.
  • 20260718161000_enforce_service_charge_invoice_charging_total.sqlservice_charge_invoice_versionsBEFORE INSERT에서 NEW.charging_confirmation_id가 가리키는 confirmation_payload.totals.allocatedNEW.invoice_payload.totals.supplyAmount를 numeric exact equality로 비교한다. 다르면 안정 오류 service_charge_invoice_charging_total_mismatch로 거부한다. 함수는 private, search_path='', 전 역할 실행권 revoke이며 기존 행을 다시 쓰거나 검사하지 않아 배포 전 데이터와 호환된다.
  • 서버가 workflow 행을 잠그고 expected revision과 request payload를 검증한다. 응답 유실 시 동일 request key로 재시도하며, scope를 다시 읽어 invoicing=confirmed이면 이미 생성된 버전 목록을 조회해 성공으로 조정한다.
  • service_charge_invoice_versions는 append-only다. 청구 재오픈은 하류 수납이 pending일 때 기존 transition_service_charge_stage(..., 'invoicing', 'reopen') 계약으로만 가능하다. 재확정은 invoice_version + 1을 생성하며 과거 버전을 수정하지 않는다.
  • 확정본 목록 조회 실패는 이미 커밋된 Fact를 실패로 되돌리지 않는다. UI는 RPC 응답의 버전을 임시 표시하고 이후 목록 조회로 복구한다.
  • 회계 전기와 전자문서 발행·재시도는 이 커밋의 성공 조건이 아니다. 관리비 UI는 공급자 이름이나 대상 상품 RPC를 알지 않는다.
  • 실제 PostgreSQL rollback smoke는 valid 100,000원 공급가액 insert가 통과하고, 내부합계가 맞는 90,000원 공급가액 payload도 linked charging allocated 100,000원과 다르면 trigger에서 거부됨을 검증한다. 별도 전체 Fact smoke는 authenticated 운영자가 같은 불일치 payload로 기존 확정 RPC를 직접 호출해도 동일 오류가 나고 workflow가 진행 상태에 남은 뒤 valid payload만 확정됨을 검증한다.

Wave 17 as-built — 독립 중간정산 Fact

중간정산(provisional)은 actual 4단계 workflow의 edition이 아니라 단건 제품 workflow다. 정본 migration은 20260718170000_service_charge_provisional_settlement_events.sql이다.

  • 필수 대상은 platform core property_spaces, 필수 entitlement는 service-charge뿐이다. lease_contracts, service_charge_stage_workflows, confirm_service_charge_facts를 참조하지 않는다.
  • service_charge_provisional_settlements는 mutable draft, ...confirmations는 settlement당 1개의 immutable Fact, ...events와 private command ledger는 append-only다.
  • list/create/update/confirm/cancel v1 RPC가 Office scope, role, product, revision, request-key payload를 검증한다. 상태는 draft→confirmed|cancelled; terminal은 reopen/수정 불가다.
  • 계산 항목·실제 단가 이력·수량은 operator explicit input이다. 서버 calculator가 cutoff의 윤년/월일수를 파생해 create/update snapshot을 만들고 confirm에서 재계산·exact 비교한다.
  • 확정 뒤 generic platform event만 best-effort enqueue한다. 회계·전자문서 delivery와 자동 전기·발행은 요청하지 않으며 event 실패도 Fact를 롤백하지 않는다.

실행 검증은 service_charge_provisional_settlement_smoke.sql, 세부 계약은 잠정정산 BE 핸드오프를 따른다.

Wave 12 상품 독립 경계

관리비의 필수 상품 계약은 service-charge뿐이다. 재무회계·전자세금계산서·CRM 계약이 없어도 관리비 편집 스냅샷, 단계 전이와 확정 원장을 생성할 수 있다.

  • 회계·전자문서 delivery는 해당 Office entitlement가 있을 때만 선택적으로 생성한다. 미신청은 관리비 확정 실패가 아니다.
  • CRM은 관리비 확정·청구·수납 계약의 선행조건이 아니며 관리비 서버 원장에 CRM FK/RPC 의존성을 두지 않는다.
  • 관리비 계정과목 표면은 회계 원장을 복제하지 않는다. FE가 재무회계 신청 여부를 확인해 회계 화면으로 이동하는 선택 링크만 제공한다.
  • 유닛 업로드는 플랫폼 공용 파일 선택 계약을 관리비 래퍼가 소비한다. 현재 파일 파싱·유닛 persist API는 연결되지 않았으며, 업로드 submit 이벤트를 최종 저장 성공으로 해석하지 않는다.

FE 경계 회귀는 scripts/service-charge-product-boundary.spec.jsconfig/product-boundaries.json이 고정한다. service-charge component/view root의 회계·전자세금계산서·CRM UI 직접 import는 0이며 제거된 세 경계를 baseline 예외로 다시 허용하지 않는다.

0. as-built 구조 (라우트·탭·페이지)

  • 라우트 팩토리(SSOT): src/composables/billingLines.js(디스크립터: 메뉴·콘솔탭) + src/router/buildLineRoutes.js(매니페스트: seg→뷰). audit-route-integrity.mjs가 무결성 검증([A]/[D]=0).
  • 제품 진입: actual은 /service-charge/actual/console/{seg}의 월별 4단계 콘솔이다. provisional은 /service-charge/provisional 단일 목록·상세 workflow이며 legacy provisional console URL은 root로 redirect한다.
  • 콘솔 탭(consoleTabs): general·charging·invoicing·collecting·notice·advance-receipt·forwarding·late-fee·tax-invoice. notice·advance-receipt는 feature 게이팅(actual만). _core 공유 콘솔(invoicing/collecting/late-fee/forwarding/tax-invoice/account/advance-receipt)은 ConsoleTabBarOrg 사용. (⚠ 라인전용 헤더 12개는 탭 하드코딩 — P-STRUCT-1 미적용, FE 부채.)
  • 드릴다운: /service-charge/{edition}/console/account/:unitKey?tab= 세대×기간 계정 허브(부과/청구/정산/연체/이월/조정 6탭, 읽기).
  • 라인레벨(에디션 무관): setting/{general,surcharge,collecting,item/{general,group},initial-data/general}, metering/general. (initial-data/group은 별칭 버그로 삭제됨.)
  • pre/post-adjustment: 라우트만 존재·메뉴 미링크(WIP). 실 조정상세 미구현(기존 멤버덤프 데드시트는 2026-06-27 제거).

1. 커밋 레이어 — 프로토 배선 상태와 BE 이관 대상

1-1. 단계 진행 상태머신 — 콘솔 [이전]/[다음]

  • as-built: useConsoleStage가 부과→청구→수납→이월 순차 상태머신을 제공하고, 로그인한 실제 Office는 consoleStageRepo를 통해 서버 상태를 hydrate한다. 브라우저 상태는 반응성/복구 사본이며 권위 상태가 아니다.
  • 서버 계약: service_charge_stage_workflows(office_id, period_management_code, edition)별 4단계 상태와 version을 보유한다. transition_service_charge_stage만 start/confirm/reopen/cancel을 수행하며 선행 확정·하류 미착수·역할을 행 잠금 안에서 재검증한다. (workflow_id, request_key) 멱등키와 service_charge_stage_transitions 감사행을 남긴다. 직접 INSERT/UPDATE 권한은 브라우저에 주지 않는다.
  • 확정 경계: 부과와 청구 확정은 각각 confirm_service_charge_facts 한 번으로 스냅샷 동결과 해당 단계 confirmed 전이를 함께 처리한다. 회계·전자문서 전달은 이 트랜잭션의 선택 delivery이며 대상 상품의 성공을 관리비 확정 성공 조건으로 삼지 않는다.
  • FE 위치: components/service-charge/*/console/*/HeaderOrg.vue 단계 카드.

1-2. 배분/청구 ActionBar — [부과시작]/[저장]/[확정]

  • as-built: actual 배분 3축의 [부과시작]/[확정]useChargingCommit에 배선됐다. useChargingCommittotalChargePrincipal <= 0 또는 totalUnallocated() > 0이면 버튼과 직접 호출을 모두 차단한다. 금액이 있고 미배분이 0일 때 serviceChargeConfirmationRepo.confirmCharging()confirm_service_charge_facts RPC 하나만 호출한다.
  • provisional 분리: 중간정산은 root 목록의 전용 repo/RPC로 create·update·confirm·cancel한다. actual ActionBar와 useChargingCommit, 월별 stage composable을 import하지 않는다.
  • 스냅샷 계약: buildChargingConfirmationSnapshot()은 활성 부과항목, 라인별 배분, 합계를 calculationVersion='service-charge-calculation.v1'로 고정한다. 화면 reactive 객체나 다른 상품 store를 payload로 전달하지 않는다. 서버는 0원·미배분·revision·권한을 다시 검증하고 확정 snapshot의 update/delete를 막는다.
  • 상태 동기화·재시도: 성공 응답의 statusesversionuseConsoleStage.applyServerConfirmation()으로 즉시 반영한다. 같은 화면 payload 재시도 동안 request key를 유지한다. 응답이 불명확하면 서버 scope를 다시 읽어 charging=confirmed이면 성공으로 조정하며, 확정되지 않았을 때만 오류를 사용자에게 표시한다.
  • 부과 금액 저장: useCharges가 실 Office·정산회차(managementCode)·edition별 부과항목·금액 스냅샷을 service_charge_snapshots에 upsert한다. leyve.billing.charges.v1도 같은 3축 키를 쓰며, 기존 2축 로컬 값은 actual로 읽는다. 로그인 시 서버 스냅샷을 우선 hydrate한다.
  • 중간저장 상태·재시도: useCharges는 현재 scope의 draftSaveStatus(idle|pending|saving|saved|error)·draftSaveError·draftSavedAt을 노출한다. 자동 debounce 저장 실패 시 실패 snapshot을 버리지 않고 같은 scope의 재시도 대상으로 보존한다. 새 편집이 먼저 생기면 더 최신 snapshot이 이전 실패본을 대체한다. actual 3축 ActionBar의 공통 ChargeDraftSaveMol이 명시 flush와 오류 재시도를 제공하며, 로컬 fallback은 이 기기에 임시 저장됨으로 서버 저장과 구분한다.
  • 확정 직전 flush: actual 확정은 pending stage transition을 기다린 다음 현재 scope의 350ms debounce snapshot 저장을 즉시 flush·await하고서 확정 RPC를 호출한다. 저장 실패 시 확정 RPC로 진행하지 않는다.
  • 청구 확정: actual 청구 ActionBar는 pending 청구시작 전이가 끝난 뒤 최신 부과 확정본과 계약축 청구 draft를 결합해 invoice Fact를 만든다. 공급가액 합은 부과 확정 totals.allocated와 같아야 한다. 이미 confirmed이면 재확정을 막고 하류 미착수 조건의 재오픈을 안내한다.
  • 회계 비용 가져오기: DialogImportChargeExpensesArt가 전기 완료 GL 잔액 중 type=EXPENSE·차변잔액 양수만 노출한다. 사용자가 비용계정→부과항목을 명시적으로 연결하며, 가져오기는 대상 항목의 baseAccruedPrincipalbaseChargePrincipal을 계정별 합계로 대체한다. 분개는 생성하지 않는다.
  • 남은 BE 경계: 중간저장 저장·상태·재시도는 완료됐다. 회계 비용 가져오기 read seam은 별도이며 전기 상태·기간·Office scope와 계정→항목 매핑을 서버에서 검증해야 한다. 클라이언트 localStorage를 권위 상태로 사용하지 않는다.

편집 스냅샷 DB 계약

  • 테이블: public.service_charge_snapshots
  • migration: 20260718150000_lock_confirmed_service_charge_snapshots.sql
  • 키: (office_id, period_key, edition) 복합 PK. period_key는 현재 정산회차의 managementCode이고 editionactual|provisional이다. 기존 행과 edition 생략 입력은 actual로 승격한다.
  • 값: charges jsonb 배열. 부과항목 정의와 입력 금액을 한 번에 복구하는 편집 스냅샷이다.
  • 감사 필드: trigger가 요청 JWT의 auth.uid()updated_by에 기록하고 updated_at을 갱신한다. 클라이언트가 감사 주체를 지정하지 않는다.
  • 접근: anon 권한 없음. 활성 Office 멤버 또는 Company 관리자가 쓰고, 같은 Company 멤버와 Office 멤버가 읽는다. UPDATE 정책은 USINGWITH CHECK를 모두 둔다.
  • 역할 경계: 이 행은 draft 복구본이다. 확정·분개·채권 생성의 권위 레코드가 아니며, 최종 커밋은 별도 트랜잭션/API가 담당한다.
  • 잠금: 동일 (office_id, period_key, edition) workflow의 charging_status='confirmed'이면 snapshot INSERT·UPDATE·DELETE를 trigger가 모두 거부한다(service_charge_snapshot_locked_after_confirmation). charging을 정식 reopenprogress로 되돌린 뒤에만 다시 쓸 수 있다. UPDATE로 잠긴 scope 밖으로 행을 옮기는 우회와 잠긴 scope 안으로 옮기는 우회도 old/new 양쪽 검사로 차단한다.
  • 검증: supabase/tests/service_charge_snapshot_lock_smoke.sql이 edition 공존, legacy actual default, confirmed 3종 mutation 차단, provisional 독립, reopen 후 update/delete 허용을 실제 Postgres에서 검증한다.
  • FE 위치: console/charging/allocation-*/blocks/ActionBarOrg.vue, _core/console/invoicing/*/blocks/ActionBarOrg.vue.

단계 워크플로 DB 계약

  • migration: 20260716233542_service_charge_stage_workflow.sql
  • scope: Office UUID + 정산회차 관리코드 + actual|provisional. 다음 차수는 새 scope라 별도 reset update가 필요 없다.
  • 상태: pending|progress|confirmed, 순서 charging → invoicing → collecting → forwarding.
  • 권한: 활성 Office 운영자/회계담당/관리자 또는 Company 관리 역할만 RPC 전이 가능. 읽기는 활성 Company/Office 멤버에게 제한한다.
  • 감사/재시도: 요청 payload가 같은 request key는 한 번만 처리한다. payload가 다르면 idempotency conflict다.

1-3. 생성 시트 — 정산회차·항목·고지서 [확인]

  • 현황: 입력 v-model 부재 + [확인]command="close"만(저장 0). 신규 생성 워크플로 불능.
  • BE 계약: 입력 모델 + 검증(필수·합계 100%(안분비율)·음수가드) + persist API. 대상: 정산회차 생성(SheetCreateConsole), 항목 생성(setting/item SheetCu), 고지서 생성(_core SheetCreateArt).
  • FE 위치: */overlays/SheetCreate*.vue, setting/item/*/overlays/Sheet*.vue.

1-4. 선택 기반 대량작업

  • 현황: 루트 리스트 체크박스 257개 전부 v-model 부재 → DataToolBar [삭제]/[다운로드]/[이월]이 만들 수 없는 선택에 의존.
  • BE/FE 계약: 선택 모델(선택 id 집합) + 대량작업 API.

1-5. 고지서 인쇄

  • 현황: InvoicePreviewArt999,999,999 정적 하드코딩.
  • BE 계약: 세대 데이터소스 + 라인아이템 바인딩. forward 미납분 표시(2026-06-27 useInvoicingDetail 배선)·고지서 표시 태그(파킹: 회계계정↔표시항목 N:1) 트랙과 연계 — docs/handoff/backend/invoicing-detail-sheet.md 참조.

1-6. 설정 저장 — setting/general

  • 현황: SheetUpdate{AccountInfo,Rounding,InterimSettlement} [확인]이 commit 없이 close만(목업). general/MainOrg 표시값 하드코딩.
  • BE 계약: 설정 persist. 참조 실배선 패턴: lateFeePolicy.setRecognitionBasis(surcharge AccountInfo), setCollectionPolicy(collecting). collecting·surcharge 설정은 이미 실배선(scope 3계층·정책 커밋) — 동일 패턴 적용.

2. 이미 실배선(BE 참조 모범)

  • 수납(collecting): useCollectingActions·useForwarding·useReceipts(FIFO 충당·선수금·가수금). 도출근거 useDerivation.
  • 연체료(late-fee): interestEngine·useLateFee·lateFeePolicy(요율·인식기준·충당순서, effective-dated). E3 발생전표.
  • 청구 집계·이월 표시: useInvoicingDetail(3축 detailFor·combineJournal·forward 배선·E1 개시분개).
  • 예산/결산(budget): budgetStructure·useBudget·useBudgetActuals.
  • 회계(GL): useBillingJournal(E1~E9)·useGeneralLedger·useFinancialStatements·useClosing.
  • 정본: docs/handoff/backend/{collecting-detail,invoicing-detail-sheet,budget,demo-seed-factory,journal-by-nature}.md.

3. 데모 품질 정리(2026-06-27 — FE, 본 트랙)

  • initial-data/group 별칭 버그 삭제 · 에디션 라벨 SSOT 통일 · AdjustmentDetail(멤버덤프) 데드 6곳 제거 · item 목록 코드체계(시스템해시 컬럼 제거·항목명 clickable) · notice 진입 IA + 데드 테이블 정리.
  • 잔여 FE 부채(별도): 콘솔 헤더 탭바 통일(P-STRUCT-1), 목록 페이지네이션 실배선·빈상태·금액 합계행(charging/invoicing), 바이트동일 시트 사본 dedup(item↔initial-data), Read↔Update 듀얼 통합.