Skip to content

장기수선계획 백엔드 핸드오프

1. 제품 경계

  • 영구 상품 키: long-term-repair-planning
  • 소유 migration: supabase/migrations/20260719014744_long_term_repair_planning_core.sql
  • 서버 product_catalog.active=false가 기본이다. 프론트 개발 검수 catalog의 prototype/internal 노출과 판매·프로비저닝 가능 상태를 분리하며, 별도 출시 결정 전 checkout/worker가 선택할 수 없다.
  • 허용 의존성은 플랫폼 공통 companies, offices, auth.users, entitlement/party 권한 함수뿐이다.
  • 관리비·시설·회계 테이블을 FK 또는 join하지 않는다. 외부 값은 source_product_keysource_reference라는 불투명 참조로만 받는다.
  • 연결 상품이 없어도 source_kind='manual' 적립·사용 원장으로 완전 동작한다.

2. 데이터 모델

테이블역할핵심 불변식
long_term_repair_plansOffice별 계획 aggregate(office_id, management_code) 고유, 서버 자동 관리코드와 현재 버전 pointer
long_term_repair_plan_versions기간·면적·개시 적립액 버전draft → confirmed; 확정 뒤 불변
long_term_repair_items수선 항목버전별 등록코드 고유, 주기/비율/수량 검증
long_term_repair_funding_scenarios균등/구간별 적립안버전별 이름 고유
long_term_repair_funding_rate_bands구간별 재원 배분율계획기간 전체를 gap/overlap 없이 덮고 합계 100%
long_term_repair_reserve_entries적립·사용 수기/투영 원장금액은 양수, Company별 request_key 고유·payload hash 일치 재시도만 replay

확정 버전의 자식 항목·시나리오·구간은 trigger가 INSERT/UPDATE/DELETE를 모두 거부한다. 적립·사용 원장은 계획 확정 후에도 실제 발생 사실을 추가할 수 있다.

관리코드는 서버가 LTR-YYYY-XXXXXXXX 형식으로 생성한다. 최초 생성 요청에 override가 있으면 영문/숫자로 시작하는 영문·숫자·.·_·- 80자 이하만 허용하며 이후 변경할 수 없다.

3. 연도별 일정 파생

private.long_term_repair_yearly_schedule(version_id)는 각 항목에 대해 다음 값을 사용한다.

  • 최초 계획연도: max(base_year, last_repair_year + cycle_years), 직전 수선연도가 없으면 base_year
  • 다음 계획연도: 최초 계획연도부터 cycle_years 간격
  • 항목 계획금액: round(quantity × unit_cost × repair_rate_percent / 100)
  • 버전 end_year를 넘는 발생분은 제외하고 연도별 금액·항목 수를 합산한다.

파생값은 별도 원장에 중복 저장하지 않고 payload 조회 때 계산한다.

검토 우선구간 projection

longTermRepairReviewWindow.js는 payload의 연도별 일정을 다시 저장하거나 계산식을 복제하지 않고, schedule을 화면 검토 범위로 투영한다.

  • 시작연도: 현재연도를 [base_year, end_year] 안으로 clamp
  • 기본 범위: 시작연도부터 최대 3개 연도. windowYears 입력으로 교체 가능하며 법정 주기를 뜻하지 않는다.
  • 빈 연도: planned_amount=0, item_count=0 행을 만들어 누락과 “예정 없음”을 구분한다.
  • 합계: 범위 안 plannedAmount, itemCount, 공사가 있는 scheduledYearCount를 파생한다.
  • 저장·RPC 추가 없음: 연도별 금액의 SSOT는 계속 private.long_term_repair_yearly_schedule(version_id)이다.

제공된 과거 계획서·사용자료에서 “다음 검토 전 가까운 수선 일정 확인” 흐름을 참고했지만, 3년은 UI 기본값일 뿐 법정 주기나 자동 준수 판정으로 하드코딩하지 않는다.

4. RPC v1

  • list_long_term_repair_plans_v1(company_id, office_id)
  • create_long_term_repair_plan_v1(...)
  • replace_long_term_repair_draft_v1(..., items jsonb, scenarios jsonb)
  • confirm_long_term_repair_plan_version_v1(...)
  • add_long_term_repair_reserve_entry_v1(...)

모든 RPC는 Company·Office scope, party 권한, long-term-repair-planning entitlement를 검사한다. 테이블은 RLS select만 허용하고 mutation은 security-definer 명령 경계로 제한한다.

  • public RPC만 authenticated에 실행 권한을 부여한다. private SECURITY DEFINER mutator는 authenticated 실행 권한을 명시적으로 회수하고 service_role에만 부여한다.
  • replaceconfirm은 동일하게 계획 aggregate 행을 먼저, 현재 버전 행을 다음 순서로 FOR UPDATE 잠근다. confirm과 draft 교체가 경합해도 확정 뒤 자식 교체가 일어나지 않는다.
  • 적립·사용 추가는 (company_id, request_key) advisory/unique gate와 canonical payload hash를 사용한다. 동일 retry는 현재 aggregate를 replay하고 다른 payload는 long_term_repair_idempotency_conflict로 거부한다.

5. 상태와 오류

  • 계획: draft | confirmed | archived
  • 버전: draft | confirmed | superseded
  • 주요 오류: long_term_repair_access_denied, long_term_repair_confirmed_version_immutable, long_term_repair_item_required, long_term_repair_funding_scenario_required, long_term_repair_rate_band_outside_horizon, long_term_repair_rate_bands_gap_or_overlap, long_term_repair_rate_bands_must_total_100, long_term_repair_idempotency_conflict.

현재 프로토타입 RPC는 최초 버전 작성·교체·확정을 제공한다. 확정본을 복제하여 v2 초안을 만드는 revision RPC는 후속 구현이다.

6. 검증

supabase/tests/long_term_repair_planning_core_smoke.sql은 기본 서버 catalog 비활성, 테스트 전용 활성화, entitlement 없음·타 Company 거부, private/direct mutation 거부, 관리코드 자동 생성/override 검증, gap·overlap·기간 밖·합계 오류, idempotent retry/conflict, 일정 파생, 확정과 확정본 변경 거부를 한 transaction에서 검증한다. longTermRepairReviewWindow.spec.js는 빈 연도 보존, 계획기간 clamp, 변경 가능한 범위 길이와 합계를 검증한다. 실제 DB smoke 실행은 disposable/local 환경에서만 하며 원격 프로젝트에는 적용하지 않는다.