다크모드
장기수선계획 백엔드 핸드오프
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_key와source_reference라는 불투명 참조로만 받는다. - 연결 상품이 없어도
source_kind='manual'적립·사용 원장으로 완전 동작한다.
2. 데이터 모델
| 테이블 | 역할 | 핵심 불변식 |
|---|---|---|
long_term_repair_plans | Office별 계획 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에만 부여한다. replace와confirm은 동일하게 계획 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 환경에서만 하며 원격 프로젝트에는 적용하지 않는다.