다크모드
관리비관리 런타임 진입과 entitlement (BE 참고 매뉴얼)
범위와 상태
관리비관리 진입 경로는 /service-charge/actual이며, 서버 capability 정본은 Company 계약과 Office entitlement의 결합이다. 2026-07-19 기준 실제 RPC·계약·Office 배정과 degraded 진입 복구가 구현됐다. ✅구현
서버 capability 불변식
private.office_product_available(company_id, office_id, 'service-charge')는 다음을 모두 요구한다.
- Office와 Company가 일치하고 Office가
active product_catalog.service-charge가 활성company_product_contracts가active- 계약 항목이
active이고 유효기간 안에 있음 office_module_entitlements가active이고 유효기간 안에 있음
get_service_charge_unit_catalog(company_id, office_id)와 get_service_charge_subject_roster(company_id, office_id)는 인증 사용자와 Office 읽기 권한까지 확인한다. 단순 메뉴 노출 또는 프런트 검수 프로필은 이 서버 capability를 대체하지 않는다.
검수 계정 프로비저닝
Migration 20260719095110_provision_service_charge_review_accounts.sql은 leysys와 goldnus Company의 활성 Office에 관리비관리 계약 항목과 entitlement를 멱등 프로비저닝한다.
- 기존 Company 계약이 있으면 같은 계약에 항목을 upsert한다.
- 가격 snapshot은
product_catalog정본을 사용한다. - 수량은 활성 Office의
unit_count합계다. - 기존 종료·중지 항목은 검수 목적에 맞게
active, 종료일 없음으로 복구한다. - 모든 활성 Office entitlement가 동일 계약 항목을 참조한다.
운영 고객을 이 migration의 slug 목록에 추가하지 않는다. 일반 고객은 가입·Sales Center 프로비저닝 흐름을 사용해야 한다.
실패 의미
호실 catalog 또는 청구 대상 명부 초기 조회 실패는 인증 실패가 아니다. 라우터는 로그인·entitlement 판정만 소유하고, 데이터 초기화 실패는 상품 UI가 degraded 상태로 표시한다. 따라서 일시적인 RPC 실패 때문에 정상 세션이나 상품 화면 진입을 취소해서는 안 된다.
검증
- 계약·항목·entitlement 상태가 모두
active private.office_product_available(..., 'service-charge') = true- authenticated 역할이
get_service_charge_unit_catalog(uuid, uuid)를 실행 가능 - 다른 Company·Office 조합은 기존 서버 권한 검사를 그대로 통과하지 못함
정기 관리비 목록 필터 계약
페이지 01-A 셸 정합에서는 서버 계약을 변경하지 않았다. 선도 제품 목록에 있던 All, tag 1, tag 2는 도메인 분류값도 API parameter도 아닌 복제 예시였으므로 제거했다.
2026-07-23 선도 제품은 useBillingPeriodListQuery 로컬 read model에서 검색·조회기간·페이지네이션을 연결했다. 이는 UI 행동과 요청 계약을 먼저 고정한 것이며 운영 서버 목록 API 완료를 뜻하지 않는다. 서버 승격 시 요청은 다음 의미를 보존한다.
| parameter | 계약 |
|---|---|
search | 명시적 검색 실행 시 적용하는 정산명 검색어 |
from_month | 포함 시작월, YYYY-MM |
to_month | 포함 종료월, YYYY-MM |
page | 1부터 시작하는 페이지 |
page_size | 20, 50, 100 중 하나 |
기본 조회기간은 요청 시점의 연도 1월부터 12월까지다. 포함 월 수는 최대 24개월이며 from_month <= to_month여야 한다. UI는 24개월을 넘는 선택에 Alert Dialog를 표시하고 직전 정상 범위로 복원하지만, 이는 편의 검증일 뿐이므로 서버가 같은 규칙을 반드시 반복해야 한다. 날짜 범위 쿼리로 변환할 때 하한은 시작월 1일 이상, 상한은 종료월 다음 달 1일 미만인 반개구간을 권장한다.
FE가 visible 조회기간 라벨을 생략하고 연월 범위 trigger와 popover를 EDS md 규격 200px로 맞춘 것은 표시 계층 변경이다. from_month, to_month, 최대 24개월 검증과 조회 실행 계약은 변경되지 않는다.
2026-07-24의 EDS 1.1.4 적용은 MonthRangePicker의 월 선택 셀을 32/40/48px 정사각형 스케일로 유지하면서 trigger와 popover 폭을 168/200/232px 규격으로 정리한 표시 계층 변경이다. 관리비 목록은 md 200px을 사용한다. from_month, to_month, 선택 완료 이벤트, 최대 24개월 검증, 조회 요청 시점에는 영향을 주지 않는다.
같은 날 EDS 1.1.6 적용으로 시간·날짜 세그먼트 내부 2px, 일 달력 0px×0px, 월·연도 선택 그리드 4px×4px 계약을 확정했다. 관리비 MonthRangePicker의 실제 가로·세로 행 간격도 4px이다. 이는 표시 계층만 바꾸며 from_month, to_month, 최대 24개월 검증, 목록·count 요청, DB schema와 API 계약에는 영향이 없다.
- 정기 관리비와 중간정산을 명시적으로 구분한다. 실제 제품의
/feeV2/maintenance-fee는is_mid_fee필터와 동일 조건의 count를 제공해야 한다. - 검색어·기간·정산 종류는 목록 행과 count에 정확히 같은 조건으로 적용한다.
- Company·Office scope와 권한 검사를 모든 목록·count 요청에 동일하게 적용한다.
- 정렬은 정산기간 내림차순 후 관리코드 또는 고유코드의 안정 정렬을 사용한다.
page_size=all또는 무제한 응답은 제공하지 않는다.- 확정 이후 삭제 차단은 필터와 별개의 서버 상태 불변식으로 유지한다.
서버 어댑터가 연결되기 전 FE 로컬 배열 결과는 프로토타입 검증용이다. 운영 완료 판정은 동일 조건의 행·전체 건수, 최대 24개월 서버 검증, 안정 정렬을 모두 확인한 뒤에만 한다.
FE의 compact pagination footer는 응답의 page, page_size, 전체 count로 first–last / count를 계산한다. 총, 건, 페이지당 visible copy 제거와 20개씩 표시는 표현 계층의 결정이며 API field를 바꾸지 않는다. 서버는 직접 페이지 입력이나 First/Last 버튼 존재를 전제로 하지 않는다.
첫 부과 준비조건과 생성 차단
첫 부과 준비조건 미충족은 목록 조회 실패가 아니다. FE는 정기 관리비 목록을 그대로 표시하고 사용자가 추가를 요청한 순간에만 Alert Dialog로 생성 차단 사유와 해결 경로를 보여 준다. 서버 API가 상시 배너 노출 여부나 닫힘 상태를 저장하지 않는다.
실제 생성·확정 API는 UI Dialog와 무관하게 면적·유닛 마스터·활성 부과항목·청구 대상 명부 불변식을 다시 검증해야 한다. 미충족 응답은 구조화된 차단 사유를 반환하고, FE의 useFirstChargeReadiness()와 같은 의미를 유지한다. 목록 read API와 count는 이 준비조건 때문에 실패시키지 않는다.
부과산정 합계가 0원일 때 FE는 반복 안내 Dialog를 자동으로 띄우지 않는다. 사용자는 편집표에서 직접 입력하거나 도구 모음의 회계 비용 가져오기 액션을 선택한다. 이는 진입 UX 변경이며 서버 요청, 편집 snapshot, 확정 불변식은 바뀌지 않는다.
정기 관리비 본문 폭과 서버 경계
2026-07-23 목록 폭 정합은 FE 레이아웃 변경이다. 정기 관리비는 다열 ERP 표이므로 wide 본문(w-full)을 사용하며, 화면→본문 gutter는 mobile 16px / EDS 24px이다. 기존 container mx-auto 최대 폭은 제거했다.
이 변경은 목록 API, 검색·기간 parameter, count, 페이지네이션, entitlement, 정산 상태머신과 금액 projection을 바꾸지 않는다. 서버가 viewport별 열 수나 본문 너비를 내려주지 않으며, FE도 좁아진 컨테이너에 맞추기 위해 응답 필드를 누락하거나 합치지 않는다. 표의 가로 overflow는 표시 계층이 소유한다.
2026-07-28의 @leysys/eds@1.6.3 scrollbar 계약도 표시 계층 변경이다. 기본 overflow와 Overlay/Native renderer의 기본 보이는 두께는 6px을 목표로 하고, 2px·4px는 명시적 compact 선택지다. 모든 renderer는 border-neutral-subtle alpha 0.8 공통 색 token과 투명 track을 사용한다. 정기 관리비 표와 페이지 헤더는 exact pseudo-element 두께 class를 붙이지 않은 전역 기본 Native를 사용해 표시 시점·자동 숨김·overlay/reserved 배치를 운영체제와 브라우저에 맡긴다. 모바일 기본 설정에서는 swipe 직후 native indicator가 나타났다가 사라지며 상시 custom rail을 만들지 않는다. 이 renderer·두께·표시 수명·색상 정책과 키보드 focus 처리는 서버 요청을 만들지 않는다. 목록의 검색·조회기간·선택·삭제·page·page_size·전체 count 계약과 행 정렬은 변경하지 않으며, 페이지네이션은 스크롤 wrapper 밖에서 기존 응답 의미를 그대로 소비한다.
정기 관리비 목록 금액 read model
월별 정기 관리비 목록은 다음 네 표시 지표를 사용한다.
| 화면 | 응답 필드 | 의미와 시점 |
|---|---|---|
| 부과액 | chargeAmount | 해당 차수의 부과 확정 금액. 진행 중이면 현재 산정값 |
| 수납액 | settledAmount | 해당 부과를 소멸시킨 금액. cashCollectedAmount + advanceAppliedAmount |
| 미수잔액 | receivableBalance | 조회 시점의 미회수 채권 잔액 |
| 수납률 | collectionRate | settledAmount / chargeAmount. 0분모이면 null, 최댓값은 1 |
cashCollectedAmount는 새 현금 유입만, advanceAppliedAmount는 기존 선수금을 이번 관리비에 사용한 금액만 나타낸다. 목록은 두 금액을 별도 열로 반복하지 않고 settledAmount로 합쳐 보여 주며, 상세 응답에서는 두 구성 필드를 유지한다. invoiceAmountDue도 청구 문서의 시점 고정 금액으로 보존하지만 월별 목록 열에서는 제외한다.
현재 FE 프로토타입은 진행 중 차수에서 계약별 forwarding·수납·연체료 read model을 총계 전용 projection으로 조합하고, 마감 시 closedSummary에 원천 구성 필드와 네 표시 지표를 고정한다. 이전 한국어 키 부과총액·수납총액·이월총액은 과거 로컬 스냅샷 읽기 호환에만 사용한다.
FE의 진행 중 차수 projection은 계약별 forwarding 행을 한 번 순회한다. 월별 목록 요청에서 호실별 고지서·수납 상세를 N건 생성해 다시 합산하는 계약은 금지한다. 운영 서버도 목록 행의 네 지표를 집계 projection 또는 저장된 read model로 직접 반환하고, 호실별 고지서 document payload를 목록 응답에 포함하지 않는다.
운영 서버 목록 API는 특히 다음 불변식을 지킨다.
settledAmount = min(chargeAmount, cashCollectedAmount + advanceAppliedAmount)다.collectionRate = settledAmount / chargeAmount이며 0분모는null이다.invoiceAmountDue는 청구 확정본의 값이며 수납 후 재계산하지 않는다.receivableBalance만 수납·충당·감면·조정에 따라 변한다.advanceAppliedAmount와advanceBalance를 혼용하지 않는다.- 과거 스냅샷에서 선수금 충당액을 알 수 없으면
settledAmount와collectionRate를null로 반환하며 현금수납액만으로 추정하지 않는다. - 목록 행·합계·상세 화면은 같은 Office·정산회차·기준시각을 사용한다.
⚠️ 서버 목록 projection 승격 전까지 로그인한 실제 Office에서 진행 중 차수의 금액은 프런트 read model이며, 운영 API 완료로 표시하지 않는다.
페이지 셸 수직 리듬과 서버 경계
2026-07-23의 페이지 셸 정합은 @leysys/eds@1.0.14가 소유하는 UI 계약이다. 전역 탐색·페이지 제목·업무 맥락·로컬 탭의 데스크톱 기준 높이는 각각 56px·64px·72px·48px이다. 업무 맥락 72px은 최소 높이이며 데이터가 줄바꿈되면 확장한다.
이 변경은 관리비 목록 API, entitlement, 검색 parameter, count, 정산 상태머신과 payload를 변경하지 않는다. 서버는 헤더 높이를 계산하거나 내려주지 않는다. 다만 긴 프로퍼티명·다국어 문자열·추가 메타데이터를 응답할 수 있으므로 FE가 72px 고정 높이로 잘라내는 것을 전제로 계약하지 않는다.
관리비 목록의 업무 맥락 disclosure는 처음 닫힌 로컬 UI 상태이며 서버 저장·API parameter·사용자 preference가 아니다. @leysys/eds@1.6.2의 DisclosurePageHeader가 Reka Collapsible 상태와 aria-expanded를 소유하고 Header는 항상 표시하며 Body·Footer만 함께 접는다. page-header-divide-y는 Header와 열린 Body 사이의 내부 1px 표시 경계만 만들고 레이아웃 높이·API·payload를 변경하지 않는다. 헤더에 실제 페이지 액션이 추가되더라도 그 액션 API와 disclosure 상태를 결합하지 않는다.
관리비 콘솔의 공용 PageHeader는 leading navigation | title/context | actions | disclosure trigger 표시 영역을 사용하며 action과 trigger를 형제로 유지한다. 이는 @leysys/eds와 FE 공용 컴포넌트가 소유하는 레이아웃 계약이며 서버 payload를 변경하지 않는다. 서버는 뒤로가기 위치, 제목 시작점, action 너비, disclosure 상태, 가로 스크롤 위치를 저장하거나 응답하지 않는다. 회차명과 Workspace명이 길어져도 원문 값을 그대로 제공하고, FE가 가운데 영역에서 줄바꿈한다.
청구 계약·유닛·멤버 명세의 조회조건·액션·표·페이지네이션은 FE data surface 안에서 16px 주요 영역 리듬으로 합성한다. 이는 표시 계층 계약이며 세 축의 목록·검색·count·pagination API, payload, 정렬·필터 의미를 변경하지 않는다. 같은 toolbar 내부 control 간격은 별도 4–8px 규칙을 유지한다.
2026-07-28 transaction surface anatomy 정리는 MainOrg의 단일 자식 pass-through wrapper를 제거하고 각 toolbar·table block root에 width·layout·overflow 소유권을 모은 FE 전용 변경이다. 청구·수납·부과·조정·설정 목록의 DOM 깊이와 scroll owner만 정리하며 검색·필터·선택·count·page·page_size, 정렬, 확정 API와 응답 schema는 바꾸지 않는다.
같은 날 보완된 toolbar shrink-0 계약은 Search·Criteria·Action·Pagination을 모바일에서도 각각 한 줄로 유지하는 FE 표시 규칙이다. 세금 태그와 날짜·확인 control은 같은 Criteria 줄에서 가로 이동하며, 줄바꿈이나 필수 control 숨김을 사용하지 않는다. paginated transaction surface는 page만 세로 scroll을 소유하고 table은 가로 overflow만 담당한다. 이 변경은 세금 구분과 기준일의 필터 의미, 요청 parameter, count·pagination 계약을 바꾸지 않는다.
2026-07-26의 @leysys/eds@1.2.3 적용은 짧은 가로 TabsList를 trigger 합산 너비로 유지하고, 긴 List를 부모 가용 폭에서 제한한 뒤 List 자체가 가로 overflow를 소유하게 한 표시 계층 변경이다. TabsRoot·TabsContent·PageHeader 밴드와 패널은 전체 가용 폭을 유지하며, plane 배경·padding·radius와 scroll viewport는 같은 TabsList가 소유한다. 제품은 scrollbar renderer·두께만 선택하고 폭·overflow 보정을 추가하지 않는다. 이 변경은 라우트, 정산 종류, entitlement, 목록·count API, 서버 payload와 DB schema를 변경하지 않는다. 서버가 탭 수나 viewport를 기준으로 응답 구조를 바꾸거나 트랙 너비를 내려주지 않는다.
라우트 진입과 초기 데이터 응답의 분리
requireAuthenticatedRoute가 동기적으로 기다리는 범위는 인증, Company·Office 컨텍스트, entitlement 판정까지다. entitlement가 확인된 뒤의 hydrateProductRuntime은 background로 시작하며 라우트 반환을 막지 않는다.
- catalog·roster 등 선택 데이터 RPC가 지연되거나 응답하지 않아도
/service-charge/actual진입은 완료되어야 한다. - 초기 데이터 실패는 인증 실패로 변환하지 않는다.
- 각 상품 화면은
loading/degraded/error상태와 재시도를 소유한다. - 서버는 동일 요청의 재시도를 안전하게 허용해야 하며, 클라이언트가 라우트 진입 완료를 위해 초기 데이터 응답을 기다린다고 가정하면 안 된다.
회귀 테스트는 끝나지 않는 hydrateProductRuntime Promise를 주입한 상태에서도 route guard가 true를 반환하는지 확인한다.
초기 동기화 성능·비파괴 계약
2026-07-23부터 관리비 런타임의 초기 read는 전사 전체가 아니라 현재 office_id로 제한한다.
- 정산 기간, 부과 snapshot, 콘솔 단계 repository의
loadAll(officeId)는 Supabase query에office_id = currentOfficeId를 적용한다. - Office 전환 시 새 Office 범위만 다시 읽는다. 다른 Office의 로컬 cache를 지우거나 같은 데이터를 재조회하지 않는다.
- 원격 값을 Vue state에 반영하는 동안 persistence watcher를 정지한다. 초기 read가
saveChain·upsert·단계 저장으로 되돌아가는 read-after-write 폭주는 금지한다. - 정산 기간·snapshot·단계·호실 catalog·청구 대상 명부는 동시에 요청하지 않고 순차 실행한다. 서버가 한 사용자의 화면 진입마다 다섯 응답을 동시에 생성한다고 가정하지 않는다.
계정 화면은 Supabase session의 사용자와 같은 사용자·Company로 저장된 로컬 계정 컨텍스트가 있을 때 먼저 열린다. getUser와 Company·Office 재수화는 background 재검증이다. 이 cache는 UI 진입 최적화일 뿐 서버 권한이 아니며, 모든 RPC/RLS는 토큰·Company·Office·entitlement를 다시 검증해야 한다.
같은 SPA에서 서버 검증을 완료한 사용자의 후속 라우트 이동은 브라우저 내 검증 컨텍스트를 재사용한다. 관리비 카드 클릭마다 getSession()을 다시 기다리지 않으므로 Supabase auth lock과 background getUser()가 겹쳐도 화면 전환을 막지 않는다. 이 경로 최적화는 API 권한을 대체하지 않는다. 만료·변조된 JWT, 다른 Company·Office 접근, 상품 미배정은 각 RPC와 RLS에서 계속 거부해야 한다.
클라이언트는 route preload와 관리비 runtime hydration을 브라우저 idle slot에 예약한다. idle 예약에는 강제 timeout을 두지 않는다. 브라우저가 바쁜데도 일정 시간이 지나면 실행을 강제하면 응답 적용이 사용자 입력과 경합할 수 있기 때문이다. API는 화면 클릭 직후 모든 관리비 read가 동시에 시작된다고 가정하지 않아야 한다. 이는 API timeout·응답 모델 변경이 아니라 첫 화면과 background read의 경합을 제거하는 호출 시점 정책이다.
청구 대상 설정 시트의 선택 UI는 후보 목록을 유닛마다 복제하지 않는다. 프론트엔드는 Office 후보 payload를 한 번 받아 현재 조작 중인 유닛에만 펼친다. 서버는 select 하나마다 후보 API를 반복 호출하는 계약을 제공하지 않으며, 이 최적화로 API·저장 모델은 변경되지 않는다.
회귀 계약:
- 현재 Office filter가 repository query에 포함됨
- 원격 hydrate 직후 저장 호출 0회
- 콘솔 단계 원격 레코드는 레코드별 Vue state 교체가 아니라 메모리에서 병합한 뒤 state·version을 각각 1회 교체
- 사용자 466명/유닛 466개 첫 화면 read model 계산 1초 미만
- 청구 대상 시트의 유닛별 select가 후보 API를 반복 호출하지 않음
- 같은 SPA의 검증 완료 후속 이동이 새로운 session API 응답을 기다리지 않음
- background 인증·상품 동기화가 route guard 반환을 막지 않음