Skip to content

프로퍼티·공간 코어 — Backend 계약

결정

Office가 Company 권한·장부·프로퍼티 설정의 정본이다. 이번 Wave는 별도 properties root를 만들지 않는다. public.property_spaces는 Office 아래의 물리·운영 공간 계층만 소유한다. Office 하나가 복수 프로퍼티를 소유해야 하는 실제 요구가 생기면 데이터·권한·과금 영향까지 다루는 별도 ADR과 마이그레이션으로 승격한다.

Unit의 계약 상한은 공간 identity와 분리한다. Organization이 특정 Office에 구매한 Unit Seatworkspace_unit_seat_subscriptions가 소유하고, 실제 활성 Unit은 property_spaces가 소유한다. 상세 결정은 docs/decisions/UNIT-SEAT-OWNERSHIP-2026-07-26.md를 따른다.

Migration: 20260717080010_property_space_core.sql

Unit Seat·운영 command migration: 20260726065805_workspace_unit_seat_management.sql

기존 모델 전수 조사

영역현재 정본/키현재 소비 지점이번 연결
Company·Officeoffices.id, company_id, code; 프로퍼티 정보는 display_name/address/unit_count/property_data계정 범위, 모듈 권한, 프로퍼티 설정그대로 root 유지
관리비property_data.units[]propertyUnitMasterunitCode=dongName-unitName으로 변환; useAllocations와 부과·검침·수납이 문자열 키 소비propertyUnitMaster, useAllocations, useMeterReadings, useForwarding, 보고서service_charge_unit_code alias → 공간 UUID
임대차tenancyMock.unitKey, useLeaseContracts.unit 문자열이 서로 독립인 mock계약/유닛/멤버 3축, 보증금·청구 prototype자동 추정 금지; 후속 lease_unit_key 명시 연결
호스피탈리티hospitality_rooms.property_space_id, 예약 allocation, stay 직접 FK룸마스터, 룸보드, 하우스키핑, 체크인·스테이hospitality_room_id alias + 직접 FK 완료
시설관리useAssetUnitsuseAllocations.units를 재사용하고 운영 상태를 문자열 unitCode overlay로 보관시설 유닛 보드·상태 시트facility_unit_code alias → 같은 유닛 UUID
빌딩 시각화별도 buildingApi complex/building/floor/unit mock3D building viewer후속 read adapter; 이번 자동 연결 없음

대규모 리네임이나 mock 일괄 교체는 하지 않는다. 이름이 같은 것만으로 임대 유닛이나 호텔 룸을 자동 병합하지 않는다.

스키마

property_spaces

  • Company·Office 복합 FK로 tenant 경계를 고정한다.
  • parent_space_id(id, company_id, office_id) 복합 FK라 다른 Office 공간을 부모로 둘 수 없다.
  • 타입: land | building | level | unit | guest_room | meeting_room | desk | common_area | parking | facility_area | other.
  • 계층 불변식: land는 root, building은 root 또는 land 하위, level은 building 하위, unit/guest_room/meeting_room/desk는 building 또는 level 하위다.
  • management_code: Office 안에서 unique, 생성 후 불변.
  • registration_code: 고객 노출 안정 코드, 중복 허용, 최초 값이 생긴 뒤 불변.
  • UUID, Company, Office, 부모, 타입, source도 불변이다. 이름·상태 변경은 revision을 증가시킨다.
  • 삭제는 금지하고 inactive/archived 상태를 사용한다.

property_space_aliases

기존 도메인 키를 공간 UUID로 해석하는 immutable 호환 테이블이다. (office_id, alias_namespace, alias_value)가 unique이며 update/delete를 거부한다.

예약 namespace:

  • public_data_unit_key: Office 건축물대장 snapshot의 원문 dongName-unitName
  • service_charge_unit_code: 현재 관리비 unitCode
  • facility_unit_code: 현재 시설관리 unitCode
  • lease_unit_key: 후속 임대 원장이 검증 후 명시 연결
  • hospitality_room_id: 룸 마스터 생성이 검증 후 직접 FK와 함께 명시 연결

namespace는 정규식으로 확장 가능하지만 새 도메인은 의미·소유권·충돌 처리 문서를 먼저 추가한다.

property_space_unit_profiles (Wave 13)

property_spaces의 unit UUID에 1:1로 붙는 공용 물리 프로필이다. 용도, 전용면적, 주거공용면적, 기타공용면적을 저장하고 공급면적·계약면적은 generated column으로 계산한다. Company·Office 복합 FK, nonnegative 면적, revision, source provenance를 강제하며 authenticated 직접 DML은 허용하지 않는다.

기존 offices.property_data.units[]office_snapshot profile로 backfill·동기화한다. 관리비 CSV로 갱신된 profile은 이후 Office snapshot trigger가 덮어쓰지 않는다. 상세 command와 보안 계약은 프로퍼티 유닛 가져오기를 따른다.

workspace_unit_seat_subscriptions

  • Organization 계약 aggregate이며 (company_id, office_id)가 unique다.
  • purchased_quantity는 해당 Workspace가 가질 수 있는 활성 Unit의 최대 개수다.
  • unit_price, currency_code, billing_interval은 Organization 계약 시 확정한다.
  • 기존 Workspace는 현재 활성 Unit 수를 grandfathered 수량으로 backfill하고 과거 가격은 추정하지 않는다.
  • 회사의 owner | contract_admin | company_admin만 계약 row를 읽고 변경한다.
  • Office 운영자에게는 get_workspace_unit_seat_capacity가 가격을 제외한 구매·사용·잔여 수량만 반환한다.
  • 수량 감액은 현재 활성 Unit 수보다 작을 수 없다.

offices.unit_count는 활성 Unit 수의 projection일 뿐 구매 수량이나 가격의 정본이 아니다.

Unit command

  • set_workspace_unit_seat_subscription: Organization 계약 권한이 Workspace별 좌석 수량·단가·통화·과금주기를 확정한다.
  • create_workspace_unit: Office 공간 관리자 권한이 단건 Unit을 생성하거나 archive된 동일 Unit을 복원한다.
  • archive_workspace_units: Office 공간 관리자 권한이 최대 500개 Unit을 한 번에 archive한다.
  • get_workspace_unit_seat_capacity: 공간 조회 권한에 구매·사용·잔여 수량만 반환한다.

enforce_workspace_unit_seat_capacity trigger는 property_spaces의 active Unit insert와 reactivation을 모두 검사한다. 따라서 단건 생성, CSV import, Office snapshot projection 등 어떤 쓰기 경로도 구매 수량을 우회할 수 없다. 동시 생성은 subscription row lock으로 직렬화한다.

archive는 물리 delete가 아니다. property_spaces.status='archived'로 전환하고 UUID·관리코드·등록코드·alias·profile·역사 참조를 보존한다. 활성 count와 offices.unit_count만 감소한다.

Office snapshot 투영

offices.property_data 변경 trigger와 sync_office_property_spaces RPC가 units[]를 다음처럼 멱등 투영한다.

text
Office
└─ building (dongName, 없으면 Office 기본 building)
   └─ level (floorName, 없으면 층 미상)
      └─ unit (dongName-unitName)

public_data_unit_key, service_charge_unit_code, facility_unit_code만 자동 생성한다. 임대·호스피탈리티 키는 동일 문자열이라는 이유로 자동 생성하지 않는다. snapshot에서 사라진 공간은 자동 삭제·archive하지 않는다. 이미 원장 참조가 있을 수 있기 때문이다.

RPC·보안·감사

  • public RPC는 security invoker; 실제 구현은 private schema의 최소 security definer이며 search_path=''를 사용한다.
  • create_property_space: Company 관리자, Office 관리자/운영자, 유효 property_admin만 실행한다. actor ID를 입력받지 않고 auth.uid()에서 유도한다.
  • bind_property_space_alias: 같은 관리자만 기존 공간 UUID에 임대차·호스피탈리티 등 검증된 legacy key를 명시 연결한다. 이름 비교나 자동 병합은 하지 않는다. 동일 요청 재처리와 같은 공간의 동일 alias 재연결은 멱등이고, 다른 공간이 이미 점유한 alias는 거부한다.
  • sync_office_property_spaces: 같은 관리 권한과 요청 키를 요구한다.
  • private property_space_commands는 공간 생성·alias 연결·snapshot 동기화의 payload+response 멱등 ledger다. 같은 키·다른 payload를 거부하고 update/delete도 거부한다.
  • public table은 RLS를 켜고 authenticated에는 SELECT만 명시 grant한다. 모든 직접 INSERT/UPDATE/DELETE는 금지한다.
  • 읽기는 활성 Company membership, 대상 Office membership 또는 유효 property_viewer/property_admin grant만 허용한다.
  • 생성·alias 연결·명시 sync는 공통 operational_audit_events에 기록한다. migration backfill과 Office snapshot trigger 투영은 시스템 projection이며 별도 사용자 이벤트를 중복 기록하지 않는다.
  • Unit Seat 계약 변경은 billing.unit_seat_subscription_changed, 단건 생성은 property.unit_created, 선택 삭제는 property.units_archived로 감사 기록한다.

Supabase 2026 Data API 기본 변경에 대비해 table/function grant를 migration에 명시했다. RLS와 grant는 별개 층으로 검증한다.

점진 전환

  1. 현재: 기존 문자열 키가 정본 소비 경로다. Office snapshot을 공간 UUID와 alias로 projection한다.
  2. dual-read: 도메인 repo가 alias로 propertySpaceId를 선택적으로 덧붙인다. Wave 13부터 공용 unit profile catalog를 우선 hydrate하며 alias/profile이 없거나 mock 상태면 기존 화면이 동작한다.
  3. direct-FK: 새 관리비·임대·호스피탈리티·시설 원장에 (property_space_id, company_id, office_id) 복합 FK를 추가하고 쓰기 RPC에서 alias와 scope를 재검증한다.
  4. legacy retire: 전체 row의 UUID coverage, 충돌 0건, 재처리 가능 migration을 확인한 뒤에만 legacy 문자열 join을 제거한다. alias는 감사·외부 연동 호환을 위해 유지한다.

owner listing, agent listing, presale은 공간의 판매/중개/분양 projection이며 코어 공간 identity에 넣지 않는다. 향후 별도 원장이 property_space_id를 FK로 참조한다.

검증

  • SQL: supabase/tests/property_space_core_smoke.sql
  • JS: propertySpaceCoreMigration.spec.js, propertySpaceCoreRepo.spec.js, propertySpaceCore.spec.js, propertyUnitMaster.spec.js
  • fresh migration reset, DB lint/advisor, 전체 unit test, build, precommit을 통과해야 한다.