Skip to content

회계 코어·회계 홈 BE 핸드오프

Wave 11 as-built — 단독 수기 후보와 원천 이벤트 inbox

정본 migration은 20260717185808_accounting_source_event_inbox.sql이다.

  • 모든 회계 read/operate/approve helper가 Company 계약과 Office accounting entitlement를 함께 검사한다.
  • submit_manual_accounting_journal_candidate_v1(...)는 authenticated 회계 운영자의 수기 후보 전용 경로다. 다른 상품의 source_type을 사칭하지 않는다.
  • receive_accounting_source_event_v1(...)는 service-role-only versioned envelope inbox다. 원천 상품 SQL object를 읽지 않고 전달받은 회사·Office·일자·통화·분개라인·snapshot만 검증한다.
  • private.accounting_source_event_consumptionsprivate.accounting_manual_candidate_commands는 full request hash와 응답을 보존하는 append-only 멱등 원장이다.

같은 producer/event/version의 exact retry는 기존 응답을 반환하고, 금액·라인·날짜·payload 중 하나라도 달라지면 conflict다. inbox가 만드는 것은 pending_review 후보뿐이며 기존 review → approve → post를 건너뛰지 않는다. operational_audit_events는 금융 inbox로 사용하지 않는다.

SQL 검증은 supabase/tests/accounting_source_event_inbox_smoke.sql, 구조 검증은 accountingSourceEventMigration.spec.js다. 기존 generic submit RPC는 호환을 위해 남아 있지만 브라우저 UI는 수기 전용 RPC와 서버 inbox 경계만 사용한다. 공개 execute 회수 여부는 별도 배포 검토 대상이다.

FE 소비 계약

  • accountingJournalRepo.submitManualCandidateV1(companyId, officeId, candidate)만 수기 Create 화면에 노출한다. 날짜·통화·라인·payload 전체를 정규화한 동일 요청은 같은 응답을 반환하고, 같은 request key의 memo·라인·날짜·payload가 하나라도 달라지면 conflict다.
  • 수기 Create 성공 결과는 pending_review 후보다. FE는 같은 기간의 후보 큐를 다시 조회하며 검토·승인·전기를 자동 수행하지 않는다.
  • 다른 업무 원천은 브라우저가 원천 store를 읽어 회계 RPC로 push하지 않는다. service worker/server delivery만 receive_accounting_source_event_v1을 호출하고, 기존 관리비 원천 표면은 읽기 전용 안내로 남는다.
  • 회계 운영 코어(전표·장부·마감)는 전자문서 상품 코드 없이 수기 후보와 server inbox 후보만으로 독립 완주한다. 개정(Wave 33, 오너 결정 2026-07-21): 발행 창구는 예외 — 모듈=상품이므로 회계 안 "전자세금계산서발행"(KcLep 위치)이 발행 원장 단일 계약을 tax-invoice 상품 게이팅 하에 선택 소비한다("창구는 상품별로 여럿, 원장은 하나" — 중복 기록 없음, entitlement는 서버 RPC가 최종 검증).

일반전표 빠른 입력 payload

회계형 빠른 입력 표는 별도 DB나 RPC를 만들지 않는다. 사용자가 선택한 동일 전표번호·동일 전표일의 2개 이상 행이 차대 일치할 때 기존 submit_manual_accounting_journal_candidate_v1 입력으로 변환한다.

  • dimensions.prototypeVoucherNumber: 빠른 입력 표의 전표 묶음 번호
  • dimensions.prototypePartnerCode: 현재 Prototype 거래처코드
  • source_type: 기존 수기 후보 계약의 manual
  • 결과 상태: 기존과 동일한 pending_review

표의 행 상태는 브라우저 작업 초안이며 서버 정본이 아니다. 후보 생성 시 서버가 날짜·기간·계정코드·한쪽 차대·전체 차대 일치를 다시 검증한다. 향후 거래처 마스터가 정규화되면 임시 코드 dimensions가 아니라 안정 ID를 별도 계약으로 추가해야 한다.

매입매출전표 payload (Wave 14)

매입매출 빠른 입력도 별도 DB·RPC 없이 기존 submit_manual_accounting_journal_candidate_v1을 사용한다.

  • 과세구분은 라인 실필드 taxCategory(과세|영세|면세|불공제)로 저장한다 — RPC의 item->>'taxCategory' 경로는 기존 그대로이며, 수기 후보 시트의 null 하드코딩만 제거해 관통시켰다.
  • dimensions.{tradeType,documentType,settlementType,partnerCode,itemName}: 매출/매입 구분·증빙·결제·프로토타입 거래처코드·품목. 부가세 신고 기초자료(매출·매입처별 집계)는 이 정규화 필드를 읽는 후속 read model로 만든다.
  • 거래처 안정 키(Wave 19): 신규 마스터 테이블을 만들지 않는다 — 기존 파티 디렉터리(master_parties UUID 정체성 + office_party_assignments.management_code Office별 유일·불변)가 정본이다. 회계 라인의 dimensions.partnerCode 의미를 관리코드로 승격했고, 그리드 코드도움이 관리코드 → 거래처명을 해석한다.
  • partner_party_id 서버 관통 완료(Wave 23) — migration 20260721150000_accounting_partner_party_fk.sql(운영 적용·스모크 PASS): 후보/전표 라인에 partner_party_id uuid references master_parties(id) 컬럼 + partial index. 라인을 쓰는 5개 경로를 한 migration에서 일괄 재정의했다 — ①submit_accounting_journal_candidate(jsonb 파싱) ②private.create_accounting_candidate_v1(수기 v1·서버 inbox 공용 파싱) ③review의 후보→전표 복사 ④correct의 역분개 복사+대체 파싱 ⑤reopen_accounting_period_v1의 마감분개 역분개 복사. 파싱은 공용 헬퍼 private.accounting_line_partner_party_id(item)(빈 값=null). 멱등 호환: 수기 v1 명령 해시는 요청 원문 기반이라 optional 필드 추가가 재시도 해시를 바꾸지 않고, corrections 정규화 payload는 partnerPartyId를 값이 있을 때만 포함해 구버전 요청 재시도의 payload 비교를 깨지 않는다. 스모크(accounting_core_smoke.sql)가 후보→전표→역분개·대체 전 경로 전파를 검증한다. dimensions.partnerCode(관리코드)는 표시·그룹 키로 병행 유지 — FE 코드도움이 관리코드 해석 시 UUID를 함께 싣는다(미해석 입력은 null 허용).
  • 거래처원장(잔액·내용·총괄 4화면)은 신규 SELECT 없이 기존 listEntries projection의 거래처 라인을 브라우저에서 그룹핑한다. 안정 ID 도입 시 이 read model의 거래처 키만 partner_id로 교체하면 된다.

날짜·월 입력의 캘린더 표시와 구분 Select의 화살표는 @leysys/eds 1.0.9 표현 계약이다. 이번 정규화와 print-ds 1.0.9 동기화는 payload와 RPC를 변경하지 않는다.

감가상각 조정 payload (Wave 20)

고정자산 대장과 감가상각도 별도 DB·RPC 없이 기존 submit_manual_accounting_journal_candidate_v1을 사용한다.

  • 조정분개 라인은 자산별 (차) 0818 감가상각비 / (대) 자산 계정의 감가상각누계액 쌍이다. 누계액 계정은 CoA controlOf 짝(0202→0203, 0206→0207, 0208→0209, 0212→0213 등)에서 파생한다.
  • dimensions.{kind:'depreciation', assetId, assetCode, assetAccountCode}: 자산 추적 키. 후속 고정자산 서버화 시 이 dimensions가 자산 FK로 승격할 자리다.
  • 상각 계산은 클라이언트 파생이다 — 정액 = 취득가액÷내용연수, 정률 = (취득가액−전기말상각누계액)×상각률(1 − 0.05^(1/n) 소수 3자리, 잔존 5% 가정 — KcLep 상각률표 동형), 월할 = 연÷12, 비망가액 1,000원 하한. 서버는 기존 수기 후보 계약의 형식·균형·기간 검증만 수행한다.
  • 참고 설계(서버 승격): fixed_assets 테이블(Office scope, 자산코드 유일, 취득·상각 조건, 상태), 월별 상각 스케줄 서버 재계산, 전기 성공 시 전기말누계액 롤포워드(현재 대장은 브라우저 메모리 스탠드인이라 같은 월 중복 초안 생성을 서버가 막지 못한다 — 검토자가 기간 중복을 확인해야 한다), 자산 처분(매각손익)·자본적지출 이벤트 계약.

자금 축 (Wave 21) — 읽기 전용·참고 설계

자금일보·예적금 현황은 신규 DB·RPC 없이 기존 listEntries 전 기간 SELECT projection만 읽는다 (쓰기 없음).

  • 자금 계정 판정은 CoA nature '예금' + 0101 현금 — 클라이언트 파생. 서버 계정과목 마스터(accounting_account_master, Wave 26 — nature 컬럼 포함)가 생겼으므로 판정 이관은 소비 RPC/뷰 승격만 남았다.
  • 계좌 마스터는 브라우저 메모리 스탠드인이다. 참고 설계(서버 승격): fund_accounts 테이블(Office scope·계좌코드 유일·은행/계좌번호/예금 계정 FK/개설·만기/이자율), 전표 라인 dimensions.fundAccountCode(계좌 차원) 정착 → 계좌 단위 자금일보·자동 대사(펌뱅킹/바로빌 계좌조회 연동 — FEATURE-COLLECTING-RECONCILIATION-CHANNELS-2026-07-10 대사 3채널과 같은 어휘) 순서로 승격한다.
  • 대장 원금 vs 계정 장부잔액 대조는 계좌 차원 부재 상태의 명시적 경계 표시다 — 서버화 후에는 계좌별 잔액 대사로 대체된다.

전자세금계산서 연계 (Wave 22) — 참고 설계

증빙 축은 신규 DB·RPC 없이 기존 라인 정규화(taxCategory + dimensions.documentType)를 소비한다. 증빙 어휘 5종(전자세금계산서·전자계산서·신용카드·현금영수증·기타)의 정본은 accountingTradeDocuments이며, 과세구분↔증빙 정합(면세=계산서/과세·영세=세금계산서)은 클라이언트 소프트 가드다 — 서버 승격 시 후보 제출 검증에 같은 규칙을 추가할 수 있으나 저장 차단이 아니라 경고 메타데이터가 정합적이다.

바로빌 연동 참고 설계(후속):

  1. e_tax_invoices 테이블(Office scope): 원천 전표 FK(entry_id)·방향(발행/수취)·공급가액/세액·거래처 사업자번호·상태머신(draft → requested → issued → transmitted → confirmed | failed)·승인번호(NTS)·바로빌 문서키. append-only 상태 이력.
  2. 발행 요청은 Edge Function 프록시(기존 VITE_BAROBILL_FUNCTION seam — 회계증빙 모듈과 공유)로만 나간다. 브라우저에서 바로빌 API 직접 호출 금지(자격증명 보호).
  3. 전표↔계산서 정합 불변식: 발행 완료 계산서의 공급가액·세액은 원천 전표 라인과 일치해야 하며, 전표가 역분개되면 수정세금계산서(취소) 발행을 요구한다 — corrections 계약과 연동.
  4. 수취(매입)는 바로빌 매입 수집 → 서버 inbox(receive_accounting_source_event_v1)로 후보 생성 — 기존 검토·승인·전기 gate를 그대로 거친다.

현황 화면의 전송 상태: 연동 대기는 위 상태머신 정착 전의 정직 표기다.

회계 발행 창구 as-built (Wave 33): journal-entry/e-tax-invoice(KcLep 전표입력 그룹 위치) 정적 목업을 발행 원장 소비로 재작성 — 매출 매입매출전표 중 계산서형 증빙(전자세금계산서·전자계산서) 거래를 create_electronic_document_issue_request(sourceProductKey accounting, sourceRecordKey accounting:trade:{entryId})로 발행 요청한다. 신규 서버 객체 0 — 기존 발행 원장(불변 스냅샷·전송 상태머신·멱등·tax-invoice entitlement 검증)을 그대로 쓴다. FE는 같은 게이트를 미러(미구독 = 정직 안내, 메뉴는 KcLep 자리 유지). 상품 정책 = ⓐ(발행은 tax-invoice 구독 필요 — 번들 전환 시 entitlement 부여 정책만 변경).

관리항목(부서·프로젝트) 축 as-built (Wave 32) — migration 20260722050000_accounting_management_item_balances_view.sql

  • 어휘 신설: 라인 dimensions.departmentName(부서)·dimensions.projectName(프로젝트) — 관리항목 2축 MVP, 이름 기반 그룹(관리항목 마스터·코드 승격은 후속). 입력 = 수기 후보 시트 라인 필드(선택). 비채용 기록: 사원 축=HR 레인 연결 후속 / 현장 축=leyve는 Office=현장이라 장부 자체가 현장 단위(본사 시점 Office 비교 축은 별도 설계).
  • view accounting_management_item_balances: 계정×기간×마감구분×부서×프로젝트 집계(security_invoker — 새 권한 표면 없음). 미입력은 null(=FE '미지정'). 잔액축 view(Wave 24)와 동형 패턴 — 관리항목 보고서의 200전표 cap 없는 원천.
  • repo seam listManagementItemBalances(bookId)(mock 동형). FE 집계·손익 판정은 계정과목 마스터 parity(CLOSING_ACCOUNT_TYPE) — 마감분개(is_closing) 제외.
  • 스모크(코어 스모크 확장·운영 PASS): departmentName 라인 → view 집계 행 + 미입력 라인 null 유지.
  • 벤치 근거: upharos 보고서 축(프로젝트·현장·부서별 손익) — KcLep 앵커 계획 T2-1(코어 무이탈 별도 메뉴).

매입매출 명세 축 서버 승격 as-built (Wave 27) — migration 20260721230000_accounting_trade_voucher_lines_view.sql

  • view accounting_trade_voucher_lines: dimensions.tradeType 마커가 있는 posted|reversed 전표의 전체 라인만 노출하는 명세 view(security_invoker = on — 기존 RLS 통과·새 권한 표면 없음·authenticated SELECT). 컬럼 = 전표 메타(entry_id/entry_number/entry_date/entry_status/entry_memo/period_key) + 라인 전체(account_code/partner_name/partner_party_id/debit/credit/tax_category/dimensions). 역분개 전표는 dimensions 복제 계약 덕에 음수 상쇄 행으로 함께 노출된다.
  • 의도적 설계: 집계·판정(공급가액/세액/과세유형)은 FE read model(vatTradeRows) 단일 수식 유지 — 읽기 전용 보고 축이라 마감분개(Wave 26)와 달리 서버 재계산 중복을 만들지 않는다. 서버는 "무엇이 매입매출 라인인가"의 범위와 cap 없는 원천만 책임진다.
  • repo seam: listTradeVoucherLines(bookId, {limit=1000, offset})(PostgREST max-rows 대비 range 페이지네이션). FE는 1000라인 × 30페이지 루프 + 상한 도달 정직 고지.
  • 스모크(코어 스모크 확장·운영 PASS 2026-07-21): trade 전표 3라인 노출·비trade 전표 제외·tax_category/dimensions.{documentType,partnerCode} 관통 검증.
  • 부가가치 서식(Wave 37, T0-3)은 이 view 위 순수 FE 파생 — 서버 계약 불변(migration·신규 객체 0). 세금계산서합계표(vatTaxInvoiceAggregate)·신용카드수령명세서(vatCardCashSummary)·신고서 요약(vatReturnSummary)은 모두 vatTradeRows에서 증빙 구분(dimensions.documentType ∈ (전자)세금계산서/계산서 vs 신용카드/현금영수증)과 과세구분(tax_category)으로 분해할 뿐이다. 위 Wave 27 "집계는 FE 단일 수식" 원칙 연장 — 서버는 원천만 책임진다. BE 승격 후보(후속): 국세청 양식 서식 자동 작성·전자신고 파일 생성은 서버 확정 계약이 필요할 때 승격.
  • 부동산임대공급가액명세서(Wave 38, T3-1)도 서버 계약 불변 — 회계 서버가 아니라 임대차 계약 캐논(lease_contract_terms·property_occupancy_periods)을 FE가 읽기 전용 소비해 파생한다(회계 core 스키마 무변경, migration 0). 과세표준 = 임대료 과세분 + 보증금 간주임대료. 간주임대료 정기예금이자율은 국세청 연도별 고시 상수(accountingLeaseSupplyStatement.DEEMED_RENT_RATE_BASIS_POINTS_BY_YEAR, 현재 FE 상수). BE 승격 후보: ①이자율 고시값을 서버 참조 테이블로 승격(감사·정본 단일화) ②과세대상 일수·일할 규칙을 서버 뷰로 확정(신고서 제출용 확정 계약이 필요할 때). 전용면적 컬럼은 계약 캐논에 exclusive_area 승격이 선행돼야 표시 가능.
  • 원천징수이행상황신고서(Wave 40)도 서버 계약 불변 — 회계 서버가 아니라 HR 급여 스토어(payroll_run·usePayrollRun.withholdingSummary)를 FE가 읽기 전용 소비해 파생한다(회계 core·HR 스키마 무변경, migration 0). HR이 급여 계산·원천징수·이체(펌뱅킹)의 system-of-record이고, 회계 원천징수 탭은 그 위 신고 서식 read-only 뷰다. 도메인 경계 엣지 accounting→human-capital=1 승인(가드 스펙). 현재 A01(근로소득 간이세액) 단일 소득구분. BE 승격 후보: ①지급명세서(직원별 상세)는 HR이 직원별 지급 seam을 노출해야 회계가 읽을 수 있다(현 withholdingSummary는 A01 요약만) ②원천징수이행상황신고서 확정 계약(제출용)이 필요할 때 서버 뷰로 승격. 소득세 계산·원천징수 로직은 HR(payroll) 소유 — 회계로 옮기지 않는다.
  • 공제받지못할매입세액명세서(Wave 41)도 이 view 위 순수 FE 파생 — 서버 계약 불변(migration·신규 객체·경계 엣지 0). vatNonDeductibleStatementvatTradeRowstradeType='매입' && tax_category='불공제'만 집계한다. ⚠ 데이터 모델 주의: 불공제 매입은 tradeVoucherLines가 부가세를 비용(subject)에 합산 기표하고 0135(부가세대급금) 라인을 만들지 않는다(KcLep '불공' 정합) → 전표에는 공급대가(공급가액+세액)만 남고 공급가액/세액 분리가 저장되지 않는다. 명세서는 공급대가 역산(netSupply = round(gross/1.1), taxAmount = gross − netSupply, 불공제=과세분 10% 전제)으로 표시한다. 크로스체크 불변식 statement.totals.gross === vatReturnSummary().purchase.nonDeductible.supply. BE 승격/데이터 확장 후보: ①불공제 사유 코드(8구분)를 매입매출전표 dimensions에 도입해야 KcLep 사유별 내역·안분/정산 서식이 가능(현재 사유 미기록 — 총계·거래처/증빙별만) ②공급가액/세액 분리가 감사·제출용으로 확정 필요하면 전표 라인에 세액을 별도 보존(현재는 비용 합산·역산)하는 스키마 확장 검토.
  • 신용카드매출전표등발행금액집계표(Wave 42)도 이 view 위 순수 FE 파생 — 서버 계약 불변(migration·신규 객체·경계 엣지 0). vatCardSalesStatementvatTradeRowstradeType='매출' && documentType ∈ (신용카드,현금영수증)만 집계한다. 과세분 합계는 vatReturnSummary().sales.cardCash와 일치(크로스체크). BE 승격/데이터 확장 후보: ①세금계산서 중복 발급분 플래그 — 카드/현금영수증과 세금계산서를 동시 발급한 거래를 표시해야 발행금액집계표에서 중복분을 차감할 수 있다(현재 전표에 미기록 → 차감 없이 발행 총액만) ②직불·기명식선불카드 세분류(카드종류 dimension). 산식은 Wave 27 원칙대로 FE 단일 유지 — 서버는 원천만 책임진다.

거래수집(T2-2) as-built — 증빙 → 전표 후보 (migration·서버객체 0)

  • 레인 경계: 원천증빙 수집·분류 원장 = 회계증빙관리(accounting-source-documents) 상품(accounting_source_documents aggregate·accountingSourceDocumentRepo). 회계 거래수집은 그 위 소비 창구 — 분류된 증빙을 회계 전표 후보로 매핑해 기존 gate(후보→검토→승인→전기)로 넘긴다. "창구는 여럿·원장은 하나".
  • 홈택스/바로빌 수집(Phase A): accountingSourceDocumentRepo.collect{TaxInvoices,CardSales,CardPurchases,CashReceipts}NOT_WIRED throw → Prototype 데모 수집으로 실배선(accountingSourceDocumentMock.collectEvidences(source, external_key) 멱등 병합, 반환 {added, skipped}). ⚠ 실 스크래핑 아님 — 실 바로빌 어댑터(makeSupabaseRepobarobill-collect-* Edge)는 연동회원 인증키·자격증명·배포 선행. 서버화 시 external_keyaccounting_source_documents.external_key 유니크 제약으로 이관(상세: accounting-source-documents-barobill.md §2-1).
  • 증빙→후보 매퍼: src/components/accounting/operating-read-model/accountingSourceDocumentCandidate.js(accounting-owned·순수, CoA만 import). usePurchaseJournal P1 라인 형태 동형 —
    • 매입: DR 비용/자산(공급가액) + DR 0135 부가세대급금(과세) / CR 상대(공급대가).
    • 매출: DR 상대(공급대가) / CR 매출계정(공급가액) + CR 0255 부가세예수금(과세).
    • 상대 계정은 결제수단(증빙 원천)별(품질 마감 ②): 세금계산서·신용카드=외상(매출 0108 외상매출금/매입 0253 미지급금), 현금영수증=0103 보통예금(현금성 수취/지급), PG·쇼핑몰 매출=0120 미수금(정산 미수). 매입매출전표 그리드 컨벤션(카드 매출=외상매출금·매입=미지급금)과 정합 — 카드 전용 계정은 CoA에 없어 비영업 계정(0120/0253) 사용. 상대 계정이 바뀌어도 차대 균형 불변(spec 가드).
    • 증빙 분류 명칭(한글)→numeric CoA 해석(resolveAccountCode, 별칭 공과금→세금과공과금·임차료→지급임차료·수수료매출→용역매출). 미분류(accountCode null)·미매핑(CoA 부재)·비정상 금액은 후보 제외(정직 — inbox에 분류 필요/계정 매핑 필요 표시).
    • 거래처 안정 키(품질 마감 ①): resolveEvidencePartyId(evidence, parties)(순수)가 증빙 bizNo(사업자등록번호)를 통합 거래처 마스터(useCounterparties)의 regNum과 대조해 master_parties UUID를 해석, 후보 전 라인 partnerPartyId에 싣는다(Wave 23 거래처원장·부가세 거래처별 집계 정합). ⚠ UUID 정규식 가드 — 데모/비UUID id는 서버 FK로 미전송(null). 동일 사업자번호 복수면 이름으로 판별. Org가 useCounterparties.loadCounterparties(officeId)로 활성 Office 거래처를 하이드레이션 후 해석(platform-core seam·읽기 전용·경계 엣지 아님).
  • 커밋 경로: 화면 OperatingTransactionCollectionInboxOrg.vuesubmitManualCandidateV1(companyId, officeId, {…})로 후보 등록(브라우저 직접 전기 안 함 — BillingIntake 읽기전용 선례 존중, pending_review 착지). 멱등: requestKey = accounting-source-documents-{evidence.id} — 같은 증빙 재제출은 같은 후보(중복 없음), payload 상이 시 accounting_manual_candidate_payload_conflict. payload에 entryMode:'evidence-intake-v1'·evidenceId·source·direction 보존.
  • 도메인 경계 엣지 accounting→accounting-source-documents=1: accounting-owned 화면이 accounting-source-documents 소유 repo SELECT seam(accountingSourceDocumentRepo)만 직접 소비 → config/product-boundaries.json accounting-source-documents sourceRootsaccountingSourceDocumentRepo.js 승격 + 가드 스펙(scripts/accounting-accounting-source-documents-boundary.spec.js, 단일 importer 고정). ⚠ 승격 후 전체 boundary audit 재실행 — baselineExcess=0·baselineStale=0 확인(교훈 준수).
  • BE 승격 후보: ①증빙→후보 매핑(계정 해석·부가세 분해)을 서버 확정 계약으로 승격(현재 FE 매퍼) ②accounting_source_document_classification_rules 서버화 시 수집 시점 자동 계정 제안 ③매입/매출 채무·채권 계정(현재 0253/0108 고정)을 거래처·증빙유형별로 세분.

기초정보관리 허브(T4-1) — BE 영향 없음

  • 회계 하위 기초정보관리 홈(/accounting/basic-info/general)은 기존 마스터 화면(거래처·계정과목·고정자산·빌링설정·개시잔액) 진입점을 모은 순수 내비 인덱스다. 신규 서버 객체·RPC·view·migration·경계 엣지 0. 각 마스터의 데이터 계약은 기존과 동일(변경 없음).
  • KcLep 5항목 완성(회사등록·환경등록·업무용승용차등록)도 BE 영향 없음 — 3종 모두 Prototype localStorage 컴포저블(accountingCompanyProfile·accountingEnvironmentSettings·accountingBusinessVehicles, platform-core)만 사용. migration·서버 객체·경계 엣지 0. BE 승격 후보(신규 서버 모델 필요): ①회사등록 → 회사 마스터(administration/onboarding) 서버화 시 사업자번호·과세유형·기수를 그 계약에 통합(현재 회계 로컬 스탠드인) ②업무용승용차 → 0208 고정자산·운행기록부와 연결한 서버 대장(업무사용비율·손금 한도는 세법 상수, 확정 신고 계약 필요 시 서버 뷰) ③환경등록 옵션 중 전기에 영향 주는 것(대차차액 자동 등)은 서버 전기 RPC 계약으로 반영(현재는 입력 편의 표시 옵션).

현재 as-built

  • useBillingJournal: 관리비 부과·수납·조정·개시잔액 전표 후보 생성
  • useGeneralLedger: 전기 상태, 전기완료 라인, 계정잔액, 계정별·일별·월별 원장, 합계잔액시산표
  • useClosing: 기간 마감·재오픈·마감분개·마감기간 전기/전기취소 게이트
  • useFinancialStatements: 손익계산서·재무상태표·포괄손익 파생
  • /accounting/overview: 위 엔진의 실제 상태를 읽기 전용으로 표시

화면 집계는 아직 브라우저 reactive singleton 프로토타입이다. 후보·승인·전기·마감의 운영 쓰기 모델은 migration과 smoke test로 관리한다.

Wave 12 as-built — 운영 장부 읽기 모델

신규 migration 없이 기존 운영 전표 SELECT 계약을 확장했다. accountingJournalRepo.listEntries(bookId, periodKey)는 이제 라인의 line_no, 계정코드·계정명, 거래처, 적요, 차변·대변, 세금구분, dimensions까지 읽는다.

accountingOperatingReadModel은 Office·기간별 전표를 다음 규칙으로 정규화한다.

  • 유효 소스는 posted | reversed다. draft | approved는 장부·시산표에 포함하지 않는다.
  • 역분개된 원전표는 상태가 reversed가 되어도 보존하고, 별도 posted 역분개 전표와 함께 포함한다. 두 전표가 상쇄되어야 감사 추적과 잔액이 동시에 맞는다.
  • 정규화 라인을 날짜·전표번호·라인번호 순으로 정렬해 분개장을 만든다.
  • 같은 라인을 계정별로 묶어 차변합계·대변합계·잔액과 거래별 누계잔액을 만든다.
  • 합계잔액시산표는 별도 저장본이 아니라 같은 계정 집계에서 파생한다.
  • 저장된 UUID Office는 운영 repo만 사용하고, 데모 Office는 명시적인 Prototype 시나리오만 사용한다. 두 소스는 합산하지 않는다.

읽기 모델은 브라우저 프로토타입 projection이다. 대량 데이터·페이지네이션·결산 확정본·기초잔액을 포함한 운영 보고서 API는 후속 서버 read model로 승격해야 한다.

Wave 13 as-built — 회계 홈 운영 집계

회계 홈은 신규 migration·RPC 없이 기존 SELECT 계약만 읽기 전용으로 소비한다.

  • accountingPeriodReadModellistCandidates(bookId, periodKey)listEntries(bookId, periodKey)를 병렬 조회해 선택 기간의 후보(pending_review | accepted | rejected)·전표(draft | approved | posted | reversed) 건수를 집계한다.
  • 당월 차변·대변 합계와 차대 일치는 Wave 12와 같은 posted | reversed projection에서 파생한다. draft/approved는 합계에 포함하지 않는다.
  • 회계기간 상태는 후보·전표에 embed된 accounting_periods.status에서 파생한다. 해당 기간에 자료가 없으면 기간 행 유무를 단정하지 않고 기간 자료 없음으로 표시한다.
  • 결산 가능 판정은 서버 상태가 아니라 화면 안내다. 검토 대기 후보·승인 대기·전기 대기·차대 불일치가 0이고 기간이 open일 때만 마감 가능을 표시하며, 실제 마감은 여전히 close_accounting_period RPC 권한 검증을 거쳐야 한다.
  • 홈은 상태 변경 RPC를 호출하지 않는다. 숫자 클릭은 해당 필터가 적용된 일반전표입력·장부 화면으로 이동만 한다.

결산 작업대 참고 설계 — 운영 원자적 마감 계약

원자적 마감 as-built (Wave 15) — migration 20260720210000_accounting_period_atomic_close.sql

  • close_accounting_period_v1(p_period_id, p_request_key, p_closing_lines, p_reason): 단일 트랜잭션에서 ① open 기간·검토 대기 후보 0건·draft/approved 0건·기간(posted|reversed) 차대 일치 재검증 ② 마감분개(one-sided 라인, 서버 균형 재검증) approved 생성 후 기존 post_accounting_journal_entry로 즉시 전기 ③ 기간 잠금 ④ entry.closing_posted·period.closed 감사 이벤트. 마감된 기간 재호출은 기존 마감분개를 반환하는 상태 멱등이다. 마감분개는 accounting_journal_entries.closing_of_period_id(기간당 1건 유일)로 식별한다.
  • reopen_accounting_period_v1(p_period_id, p_request_key, p_reason): 사유 필수. 기간을 먼저 열고 마감분개의 역분개를 전기한 뒤 corrections 계약(kind: period_reopen)으로 posted → reversed 가드를 통과시킨다 — append-only, 물리 삭제 없음. period.reopened 감사 이벤트.
  • 한계(참고 설계 잔여): 명목계정 분류는 서버 계정과목 마스터 부재로 클라이언트가 산출한다 → ✅ Wave 26에서 해소 — 아래 "계정과목 마스터 서버화 as-built" 참조. 마감분개는 서버가 직접 재계산하고 클라이언트 라인은 교차검증 입력으로 강등됐다.
  • 개시잔액(Wave 16)은 신규 계약 없이 수기 후보로 처리한다 — dimensions.kind='opening-balance' 개시분개. 기수 이관은 마감분개+누적 파생으로 emergent (정본: docs/decisions/ACCOUNTING-OPENING-BALANCE-CARRYFORWARD-2026-07-20.md). BS 한정 검증은 Wave 29에서 서버 승격 — migration 20260722030000_accounting_opening_balance_bs_guard.sql: private.assert_opening_balance_lines_v1이 수기 후보 v1 진입 래퍼에서 개시잔액 마커 라인을 마스터와 대조한다(미등록 = opening_balance_account_master_missing / 명목계정 = opening_balance_bs_account_required, detail에 코드 목록). 마커 없는 일반 수기 라인·시스템 원천 경로는 무변경. 운영 적용·코어 스모크 확장(명목 거부 + BS 통과) PASS.
  • 스모크: supabase/tests/accounting_period_close_smoke.sql (rollback 기반 — 마감·멱등·재오픈·명목잔액 복원). ✅ 2026-07-21 운영 적용 완료: migration 20260720210000 push → advisor(신규 객체는 의도된 SECURITY DEFINER 경고뿐) → 스모크 2종 PASS. 첫 운영 스모크가 두 결함을 적발·정정했다 — ① 스모크 대상 선택이 Wave 11 entitlement 게이트를 반영하지 않아 회계 미구독 Office를 골랐던 드리프트(스모크 selection에 office_product_available 조건 추가) ② reopen이 closed_by/closed_at을 비우지 않아 accounting_periods_close_fields_check 위반으로 재오픈 전체가 실패하던 결함 → 정정 migration 20260721083000_accounting_period_reopen_clear_close_fields.sql(운영 적용·스모크 재PASS).

계정과목 마스터 서버화 as-built (Wave 26) — migration 20260721210000_accounting_account_master_close_recalc.sql

  • accounting_account_master: 전역(Office 무관) 계정과목 참조 테이블 — account_code(pk)·name_ko·account_type(ASSET|LIABILITY|EQUITY|REVENUE|EXPENSE check)·normal_balance(DEBIT|CREDIT check)·nature(더존 구분, nullable)·pack. 시드 = FE SSOT(chartOfAccounts.js CORE 333 + chartOfAccountsPresets/serviceCharge.js 22 = 355행, last-wins dedupe). RLS enable + authenticated SELECT 정책만 — 쓰기는 migration 전용(고객 편집 없음, KcLep 표준 마스터). 산업 variance pack(건설·제조·분양)은 운영 마감 경로가 소비하지 않아 시드 제외(후속 승격). 시드↔FE SSOT drift는 vitest accountingAccountMasterMigration.spec.js가 전 행(코드·명·유형·방향·nature·pack) 대조로 가드한다.
  • close_accounting_period_v1 서버 재계산 승격(시그니처 불변): 서버가 기간 posted|reversed 라인 × 마스터로 KcLep 3단(수익·비용 → 집합손익 0400 → 미처분이익잉여금 0377) 마감분개를 직접 재계산해 전기한다. p_closing_lines는 교차검증 입력으로 강등 — 계정별 순액(round 2dp)이 서버 재계산과 다르면 closing_lines_mismatch(FE 프리뷰 stale 적발), 기간 라인에 마스터 미등록 코드가 있으면 closing_account_master_missing(분류 불가 시 마감 차단, detail에 코드 목록). 이전 마감분개+역분개 쌍은 합산에서 상쇄되므로 별도 제외가 없다. 게이트·멱등·감사·reopen 계약 불변.
  • 재마감 정합 정정 동승(스모크 확장이 적발한 잠재 결함): 기존 closing_period_unique 전면 유일 인덱스가 reopen(마감분개 reversed) 후 재마감 insert를 duplicate key로 막았다 — 인덱스를 status <> 'reversed' 부분 유일("기간당 활성 마감분개 1건")로 재정의하고, 멱등 반환도 status = 'posted' 활성 마감분개로 한정했다. mock repo도 동형 정정.
  • 스모크 확장(운영 PASS 2026-07-21): 마스터 시드 355행 확인 → 교차검증 거부(closing_lines_mismatch) → 클라이언트 라인 없는 서버 단독 재계산과 마감분개 4라인 형태(0412 차 1000 / 0400 양변 / 0377 대 1000) 검증 → 멱등 → 재오픈 → 교차검증 통과 재마감 + 재마감 후 멱등. 운영 실측: 마스터 355행(명목 124계정)·부분 유일 인덱스 반영.
  • FE 배선: 마감 산식 parity SSOT src/composables/accountingClosingEntryLines.js(서버 수식과 동형·mock repo도 같은 함수로 재계산), 결산 작업대·회계 홈 잔액은 서버 view(accounting_account_period_balances)로 승격(폴백 유지), 프리뷰 라인은 서버 잔액일 때만 교차검증으로 전송(폴백 200cap 상태에서는 미전송 — 서버 단독 재계산).

운영 재무제표 read

외부보고용 손익계산서·재무상태표도 신규 migration·RPC 없이 같은 SELECT 계약을 읽는다. listEntries(bookId, null) 전 기간 조회로 기초잔액을 이전 기간 전표 누적으로 파생하며, 마감분개가 없는 동안 누계 당기순이익을 자본에 합성 표시해 대차평형을 유지한다. 200전표 cap·브라우저 projection이므로 확정 보고서·대량 기간은 서버 read API(기초잔액 snapshot 포함)로 승격해야 한다.

인쇄 계약 — 서버 불변 원본 as-built (Wave 28, migration 20260722010000_accounting_print_documents.sql)

전표·총계정원장·합계잔액시산표는 화면의 정규화된 projection을 sessionStorage의 단기 payload로 /accounting/print-preview에 전달한다(렌더링은 FE). 서버 원본 문서·감사 아카이브 부재Wave 28에서 해소:

  • accounting_print_documents: append-only 인쇄 원본 테이블 — document_no(장부 단위 채번·book 행 잠금 직렬화), document_kind(slip|general-ledger|trial-balance check), period_key, entry_id(slip만), payload(계약 accounting.print.v1), content_hash(sha256 hex 64 check), ledger_verification(서버 재검증 결과), (book_id, request_key) 멱등 유니크. RLS = can_read_accounting_book SELECT만, 쓰기는 RPC 전용.
  • create_accounting_print_document_v1(book, request_key, kind, period_key, payload, entry_id): can_operate_accounting_book 권한 → 종류별 원장 재검증(slip=전표 라인 합계 posted|reversed 필수 / general-ledger=기간×계정 라인 합계 / trial-balance=기간 라인 합계 — 불일치 = print_payload_ledger_mismatch) → sha256 해시·문서번호 발급 → print_document.created 감사. 같은 요청키 재호출은 payload 해시 일치 시 기존 문서 반환, 다르면 accounting_print_idempotency_conflict. Wave 26 마감 교차검증과 같은 "클라이언트 산출 + 서버 재검증" 패턴.
  • 불변: guard_accounting_print_document 트리거가 UPDATE/DELETE를 전면 차단(print_document_immutable, 예외 전이 없음) — 정정은 새 문서 발급으로만.
  • 스모크(코어 스모크 확장·운영 PASS 2026-07-22 KST): 생성(해시 64hex·문서번호)·멱등·원장 불일치 거부·불변 트리거.
  • 보관 원본 목록 화면Wave 30 완료ledger/print-archive 화면이 listPrintDocuments seam을 읽기 전용 소비한다(신규 서버 객체 0·쓰기 RPC 0, 스펙 가드). 보관 원본 재출력Wave 31 완료 — 단건 조회 seam loadPrintDocument(documentId)(payload 포함 SELECT·print_document_not_found)로 보관 payload를 미리보기에 재렌더한다(새 문서 발급 없음·신규 서버 객체 0). 잔여(후속): 전자서명, 고정 PDF 파일 생성·보관(스토리지).

운영 스키마 as-built

  • accounting_books: Office당 1개 장부
  • accounting_periods: 장부별 월 회계기간과 open/closed 상태
  • accounting_journal_candidates + _lines: 원천 이벤트의 멱등 검토 후보
  • accounting_journal_entries + _lines: draft/approved/posted/reversed 전표
  • accounting_audit_events: 후보 제출·검토·승인·전기·마감 append-only 이력. Wave 36에서 FE 조회 소비 개시listAuditEvents(bookId, {limit, offset}) seam이 id,book_id,action,entity_type,entity_id,reason,event_payload,created_atcreated_at desc로 SELECT한다(신규 서버 객체 0 — 기존 RLS read policy can_read_accounting_book + (book_id, created_at desc) 인덱스 그대로). actor_user_id는 v1에서 조회하지 않는다 — 표시명 없이 UUID만 있어 고객 표면 비노출 원칙과 충돌하므로, "누가" 표시는 프로필 표시명 join view(또는 RPC) 신설이 선행돼야 한다(후속 확장 포인트). action 어휘(2026-07-21 기준 전수): candidate.{submitted,accepted,rejected,source_event_received} · entry.{approved,posted,reversed,reversal_posted,replacement_created,closing_posted} · period.{closed,reopened} · print_document.created.

브라우저 역할은 모든 테이블에 SELECT만 가진다. 쓰기는 다음 SECURITY DEFINER RPC만 허용하며, 각 함수는 auth.uid()와 Office 역할을 내부에서 다시 확인한다. anon 실행권은 없다.

  1. ensure_accounting_book
  2. submit_accounting_journal_candidate
  3. review_accounting_journal_candidate
  4. approve_accounting_journal_entry
  5. post_accounting_journal_entry
  6. close_accounting_period

RLS는 7개 테이블 모두 활성화됐다. authenticated는 SELECT만, anon은 권한이 없다. 실제 Postgres 17 실행과 supabase db lint를 통과했고, supabase/tests/accounting_core_smoke.sql이 전체 상태머신을 실행한 뒤 rollback 및 잔존 0건을 검증한다.

운영 데이터 계약

JournalCandidate

필수 필드:

  • id, book_id, period_id
  • source_type, source_id, event_type, idempotency_key
  • occurred_at, documented_at, currency
  • accounting_journal_candidate_lines[]: account, partner, debit, credit, tax category, memo, dimensions
  • source_payload, status, review_reason, reviewed_by, reviewed_at

상태: pending_review → accepted | rejected. 후보는 원천 상태를 변경하지 않는다.

검토 큐 read/write 계약

  • 목록은 accounting_books.office_id로 장부를 먼저 찾은 뒤 accounting_journal_candidates.book_idaccounting_periods.period_key를 명시적으로 필터한다.
  • 목록은 최근 증빙일 순 최대 200건이며, 금액 표시를 위해 후보 라인의 차변·대변만 embed한다.
  • 상세는 후보 1건과 accounting_journal_candidate_lines를 각각 ID 필터로 읽는다. 브라우저 역할은 SELECT만 가지며 RLS가 Office 멤버십을 다시 제한한다.
  • 승인·반려는 오직 review_accounting_journal_candidate RPC를 사용한다. 승인에는 accounting_approver 역할과 open 기간·차대 일치가 필요하고, 반려에는 사유가 필수다.
  • 승인 결과는 accounting_journal_entries.status = 'draft'다. 후보 승인을 장부 전기로 해석하면 안 된다.
  • 이번 UI 슬라이스는 스키마·권한·RPC를 변경하지 않고 기존 운영 계약만 소비한다.

JournalEntry

상태: draft → approved → posted → reversed. posted 이후 라인은 불변이며 정정은 reversal entry와 replacement entry로 남긴다.

Posted 정정 계약

신규 migration 20260716232604_accounting_journal_corrections.sql은 전기완료 원전표를 수정·삭제하지 않고 다음 원자적 상태를 만든다.

  • 원전표: posted → reversed. 원전표 라인은 그대로 보존하며 후속 운영 원장은 posted | reversed 원전표를 모두 유효 소스로 포함해야 한다.
  • 역분개 전표: 원전표 라인의 차변·대변을 바꿔 열린 대상기간에 즉시 posted로 생성하고 reversal_of_entry_id로 원전표를 참조한다.
  • 선택적 대체전표: 사용자가 확정한 수정 라인으로 draft를 만들고 replacement_of_entry_id로 원전표를 참조한다. 이후 기존 승인·전기 RPC를 그대로 통과한다.

accounting_journal_correctionsrequest_key, 정규화된 request_payload, 원전표·역분개·대체전표, 대상기간, 정정일, 필수 사유, actor/time을 한 묶음으로 저장한다. (book_id, request_key)와 원전표·역분개·대체 관계는 각각 유일하다. 동일 key+payload 재시도는 기존 결과를 반환하고, 같은 key의 다른 payload는 accounting_correction_idempotency_conflict로 거부한다.

공개 쓰기 RPC:

  1. correct_accounting_journal_entry

이 RPC는 auth.uid(), 장부 승인권한, 원전표 상태, 역분개 전표 재정정 금지, 대상기간 open, 정정일 범위, 사유, replacement 차대 일치를 서버에서 검증한다. SECURITY DEFINER와 빈 search_path를 사용하며 anon/PUBLIC 실행권은 없고 authenticated만 명시적으로 실행할 수 있다.

DB 방어선:

  • accounting_journal_corrections도 RLS 활성, 브라우저는 SELECT만 허용한다.
  • posted/reversed 엔트리 삭제와 회계 필드 변경을 trigger가 거부한다.
  • posted/reversed 엔트리 라인의 INSERT/UPDATE/DELETE를 trigger가 거부한다.
  • post_accounting_journal_entry는 전기 직전에 라인 수·차대 균형을 다시 확인한다.
  • 감사 이벤트: entry.reversed, entry.reversal_posted, 선택 시 entry.replacement_created. 기존 replacement 승인·전기는 entry.approved, entry.posted를 사용한다.

✅ corrections migration(20260716232604)은 운영 Supabase에 적용돼 있다(2026-07-21 확인). rollback smoke(accounting_core_smoke.sql)도 운영 linked project에서 PASS.

승인·전기 큐 read/write 계약

  • 목록은 accounting_journal_entries.book_idaccounting_periods.period_key를 명시적으로 필터하고 최근 전표일 순 최대 200건을 읽는다.
  • 목록 금액은 accounting_journal_entry_lines의 차변·대변만 embed해 합산한다. 원천 표시는 candidate_id 관계의 source_type, source_id, event_type을 사용한다.
  • 상세는 전표 1건과 accounting_journal_entry_lines를 각각 entry_id로 필터해 읽는다. 브라우저는 SELECT만 가지며 Office 장부 RLS가 다시 제한한다.
  • draft → approvedapprove_accounting_journal_entry RPC만, approved → postedpost_accounting_journal_entry RPC만 호출한다. 테이블 UPDATE는 허용하지 않는다.
  • 두 RPC 모두 auth.uid(), 회계 승인 역할, open 기간을 확인한다. 승인 RPC는 차대 일치도 다시 검증한다.
  • 전기 시 (book_id, period_id) 안에서 다음 entry_number를 부여하고 entry.posted 감사 이벤트를 남긴다. 이미 전기된 전표 재호출은 기존 번호를 반환한다.
  • 이번 슬라이스는 기존 스키마·권한·RPC를 변경하지 않고 RLS SELECT와 RPC 쓰기 계약만 소비한다.

불변식

  1. 전표별 차변 합계 = 대변 합계.
  2. (office_book_id, idempotency_key)는 유일하다.
  3. 마감기간에는 신규 전기·전기취소를 금지한다.
  4. 전기·역분개·마감·재오픈은 actor, at, reason, request id를 감사로그로 남긴다.
  5. 원천 모듈 삭제/취소가 전기완료 전표를 물리 삭제하지 않는다.
  6. 재무제표는 오직 전기완료 원장에서 파생한다.

다음 BE 우선순위

  1. 정정 migration 운영 적용·advisor·rollback smoke ✅ 완료(2026-07-21)
  2. 운영 장부 read model의 서버 projection·페이지네이션·기초잔액 지원 — 2단계 완료(Wave 24): ①listEntries {limit,offset} range + countEntries(Wave 23) ②전표 큐 '더 보기' UI 소비 ③서버 잔액 projection view accounting_account_period_balances(migration 20260721170000·운영 적용) — 계정×기간×마감구분(posted|reversed) 집계·security_invoker = on(새 권한 표면 없음·기존 RLS 통과)·authenticated SELECT. 기초잔액은 기간 비교로 파생(200전표 cap 없음). 3단계 완료(Wave 25): 재무제표/시산표 잔액 소비를 view 원천으로 교체(200전표 cap 제거·라인 폴백 명시) + 구라인 partner_party_id backfill migration 20260721190000(불변 트리거를 마이그레이션 트랜잭션 안에서만 일시 해제 — 금액·계정 불변 1회 예외·운영 적용·트리거 재활성 확인). 4단계 완료(Wave 27): 매입매출 명세 view accounting_trade_voucher_lines(migration 20260721230000·운영 적용·코어 스모크 확장 PASS) — 부가세·전자세금계산서 명세의 200전표 cap 제거. 5단계 완료(Wave 28): 서버 불변 인쇄 원본 accounting_print_documents + create_accounting_print_document_v1(위 "인쇄 계약" as-built) — 본 서버 승격 로드맵 항목 종결.
  3. 원자적 마감 migration 운영 적용·advisor·스모크 ✅ 완료(2026-07-21 — reopen close-fields 정정 포함) / 계정과목 마스터 서버화(마감분개 서버 재계산 승격)완료(Wave 26, 2026-07-21) — migration 20260721210000(마스터 355행 시드+서버 재계산+재마감 인덱스 정정) 운영 적용·스모크 PASS. 개시잔액 BS 한정 검증도 Wave 29에서 서버 승격 완료(migration 20260722030000). 자금 nature 판정은 의도적 FE 유지 결정 — 읽기 전용 조회 축이고 판정의 정본 데이터(nature)는 이미 서버 마스터에 있으며(시드 parity 가드), 쓰기 게이트가 없어 서버 이관 실익이 없다. 자금 축이 서버 쓰기(계좌 마스터 영속 등)를 얻을 때 함께 승격한다.
  4. 개시잔액·기수 이관
  5. 거래처·증빙·부가세 신고축 정규화
  6. 고정자산 서버화 — fixed_assets 대장·상각 스케줄 재계산·전기말누계액 롤포워드·기간 중복 초안 가드