Skip to content

관리비관리 런타임 진입 (FE 메인테이너 매뉴얼)

라우트와 진입 계약

  • Workspace 카드: PRODUCT_MODULES.service-charge.consoleEntry
  • 진입 경로: /service-charge/actual
  • 라우트 생성: buildLineRoutes('service-charge')
  • 인증 경계: requireAuthenticatedRoute

requireAuthenticatedRoute는 인증과 상품 entitlement를 판정한다. hydrateProductRuntime 실패는 더 이상 /workspace/home?unavailable=...로 리다이렉트하지 않는다. 런타임 데이터 실패는 인증 실패가 아니므로 경고를 남기고 원래 상품 화면 진입을 허용한다.

관리비 런타임 초기화

productRuntimeBootstrap.jsservice-charge bootstrap은 다음을 초기화한다.

  1. 정산 기간
  2. 부과 snapshot
  3. 콘솔 단계
  4. Office 호실 catalog sync
  5. Office 청구 대상 명부 sync

앞의 세 저장소는 원격 실패 시 로컬 상태를 유지한다. 호실 catalog와 대상 명부 초기화 실패는 각각 serviceChargeUnitCatalogSyncError, serviceChargeSubjectRosterSyncError에 원인을 보존하고 bootstrap 자체는 정상 종료한다. disposer는 두 sync watcher를 함께 정리한다.

화면 degraded 상태

components/service-charge/actual/MainOrg.vue가 두 sync error를 소비한다.

  • 오류가 있으면 role=alert 경고와 정규화된 사용자 메시지를 표시한다.
  • 다시 시도는 unit catalog 또는 subject roster retry 함수를 호출한다.
  • 재시도는 기존 watcher를 정리한 뒤 현재 Company·Office로 새 catalog sync를 시작한다.
  • 성공하면 오류 ref가 비워지고 경고가 사라진다.
  • 실패하면 같은 화면을 유지하고 최신 오류를 표시한다.

페이지 로컬 CSS나 디자인 override는 추가하지 않았다. alert와 button은 EDS 공식 클래스를 사용한다.

첫 부과 준비조건의 행동 게이트

MainOrg.vueuseFirstChargeReadiness()ready, setupSteps, blockers를 한 번만 소비한다. 준비조건 미충족은 페이지 진입 오류가 아니므로 목록 위에 상시 Alert나 role=status 영역을 만들지 않는다. 검색·조회·표 레이아웃은 준비 여부와 관계없이 같은 위치를 유지한다.

DataToolBarOrgrequest-create를 받은 순간에만 requestCreate()가 조건을 판정한다.

  • ready=true: sheet-create-console-art를 연다.
  • ready=false: dialog-first-charge-readiness-art Alert Dialog를 연다.
  • Dialog 제목은 정기 관리비를 추가할 수 없습니다이며 남은 setupSteps의 메시지와 메뉴 경로를 표시한다.
  • 첫 단계가 route이면 RouterLink, 청구 대상 명부이면 lazy mount 후 전용 sheet를 연다.
  • 추가 버튼은 disabled 처리하지 않는다. 실행 시점에 차단 이유와 해결 행동을 함께 제공해야 하기 때문이다.

Dialog는 자동으로 열리지 않으며 페이지 진입·readiness 변경을 감시해 노출하는 로컬 상태도 두지 않는다. 회귀는 actual/__tests__/EntryAvailability.spec.js가 상시 status 부재와 request-create → showModal()을 함께 검증한다.

PageBody와 패딩 소유권

views/service-charge/actual/IndexView.vue는 정본 registry의 PageBody를 사용한다. PageBody는 다음 두 책임만 소유한다.

  • 화면→본문 gutter: p-4 sm:p-6(16/24px)
  • 콘텐츠 폭 tier: 기본 wide = w-full

components/service-charge/actual/MainOrg.vuew-full flex flex-col gap-4 composition만 소유한다. 다열 ERP 목록에 부적합한 container mx-auto는 제거했다. 카드 안쪽 padding은 card card-md이며 Search·Criteria·Action·Table·Pagination은 pass-through wrapper 없이 직접 합성한다. 역할이 다른 주요 영역 사이는 모두 gap-y-4(16px)로 통일한다. 검색 입력+직접 실행 버튼은 하나의 제어이므로 gap-0.5가 소유한다.

정본 파일은 leysys-design/src/registry/vue/page-body/이고 제품 copy는 src/registry/vue/page-body/이다. 두 파일은 byte-identical로 유지한다. 페이지에서 PageBody의 gutter를 다시 p-*로 덮거나, wide 목록 안에 container·max-w-* mx-auto를 추가하지 않는다. 폼·설정은 width="focused", 문서·채팅은 width="prose"를 명시한다.

SheetContent도 정본과 함께 size="md" 기본 계약을 갖는다. surface 내부 padding은 surface size가 소유하며 PageBody와 합치지 않는다.

구조 회귀는 components/service-charge/actual/__tests__/PageLayout.spec.js가 막는다. IndexView의 기본 wide PageBody 소비와 MainOrgcontainer·mx-auto 부재를 함께 검증한다.

목록 도구의 표준 순서와 크기

MainOrg.vue는 목록 도구를 다음 DOM·시각 순서로 조합한다.

text
Search → Criteria → Action → Table → Pagination
  • Search: 자유 텍스트 입력과 명시적 실행. SearchToolBarOrginput-group-lgbutton-lg를 사용한다.
  • Criteria: 아래 결과 집합을 바꾸는 구조화된 조회조건. state, status, filter 대신 코드와 문서에서 Criteria를 정식 명칭으로 쓴다.
  • Action: 선택 삭제, 청구 대상 설정, 추가처럼 레코드 또는 선택 집합을 바꾸는 행동이다.
  • Table: 현재 적용된 search·criteria의 행만 받는다.
  • Pagination: 같은 결과 집합의 범위·전체 개수·페이지 크기와 이전·숫자·다음 탐색을 표시한다. Table에 붙는 footer이므로 외부 간격을 두지 않는다.

useBillingPeriodListQuery.js가 적용 전 draft와 적용된 query를 분리한다. 기본 기간은 현재 연도 1월–12월, 최대 범위는 포함 24개월이다. CriteriaToolBarOrg는 완성된 선택 범위를 즉시 검증한다. 24개월을 넘으면 EDS Alert Dialog를 열고 직전 정상 범위를 복원한다. 상시 도움말 문장과 InfoHint는 표시하지 않는다.

크기 계약:

제어EDS 계약실측
검색 입력·검색 버튼lg36px 높이
Criteria 입력·조회 버튼sm28px 높이·12px 글자
Action 버튼sm28px 높이·12px 글자
페이지 버튼·페이지 크기 Selectpagination-sm + select-sm28px 높이·12px 글자
범위/전체 1–20 / 135body-xs12px 글자·16px 행높이

직접 결합된 input+button만 gap-0.5를 사용한다. 조회button-md로 키우면 Action과 숫자 스케일이 다시 어긋난다. 페이지 크기는 별도 페이지당 라벨 대신 Select 값 자체를 20개씩으로 표시한다.

페이지네이션 정본은 leysys-designDATA-GRID-PAGINATION-COMPACT-DEFAULT-2026-07-23.md다. 기본 조합은 범위/전체 + 페이지 크기 + Previous/숫자/Next이며 , , 페이지, 페이지당은 visible copy에서 제거하고 완전한 의미를 aria-label에 둔다. 직접 페이지 입력과 First/Last는 opt-in이다. 제품은 SSOT src/registry/vue/pagination/을 byte-identical 복사해 사용한다.

정기 관리비 표의 가로 스크롤 계약

@leysys/eds@1.6.3은 scrollbar renderer와 보이는 두께를 분리한다. 기본 overflow와 Overlay/Native의 기본 보이는 두께는 6px을 목표로 하고, 2px·4px는 명시적 compact variant다. 모든 renderer의 thumb는 공통 --scrollbar-thumb-color를 사용하며, 색은 border-neutral-subtle alpha 0.8로 Cloudflare Developer Docs의 6px·반투명 인지 농도를 EDS semantic 체계에 맞게 번역한다. track은 투명하다. 표시 수명은 별도 축으로 Overlay는 Reka type, class 없는 전역 기본 Native는 운영체제와 브라우저가 소유한다.

  • components/service-charge/actual/blocks/DynamicTableOrg.vue<div class="w-full min-w-0 overflow-x-auto"> → table 조합을 사용한다. 이 목록은 손가락 swipe가 주 조작인 일반 업무 목록으로 재분류해 상시 type="auto" custom rail과 exact 두께 class를 제거했다.
  • components/service-charge/actual/HeaderOrg.vue.page-header-meta도 별도 scrollbar class 없이 같은 전역 Native를 소비한다. 한 모바일 화면의 헤더와 표가 같은 6px 목표·투명 track·반투명 thumb·OS 자동 숨김 수명을 사용한다.

관리비 콘솔 PageHeader 정본

  • actual·provisional과 공유 _core 콘솔의 route별 HeaderOrg.vuecomponents/billing/_core/console/BillingConsolePageHeaderOrg.vue만 소비한다.
  • 공용 헤더는 @leysys/eds@1.6.2PageHeaderWrapper 안에서 정적 PageHeader 또는 DisclosurePageHeader를 선택한다. 두 변형은 같은 Header / Body / Footer anatomy를 쓰며, breadcrumb·상단 보조 맥락은 Above, 로컬 탭은 Below 형제 band로만 합성한다.
  • DisclosurePageHeaderPageHeader의 size·inset·divider surface와 Disclosure의 open/close·trigger·indicator behavior를 합성한다. 관리비 콘솔은 divide-y prop으로 page-header-divide-y를 사용하며, 제품 로컬 CSS나 disclosure-page-header-divide-y 같은 중복 variant를 만들지 않는다. 내부 구분선은 inset box-shadow라 Header 64px과 Body 72px 최소 높이를 바꾸지 않는다.
  • 제목 행은 선택적 .page-header-leading-group(뒤로가기가 있을 때 고정) → .page-header-headline-group(유동 회차명·Workspace·상태) → 선택적 .page-header-action-group(실제 액션) → .disclosure-page-header-trigger 순서다. 실제 액션과 disclosure trigger는 형제이며 버튼을 서로 중첩하지 않는다.
  • 루트 화면처럼 뒤로가기가 필요 없으면 show-back="false"로 leading 영역 자체를 렌더하지 않는다. 빈 leading 영역으로 들여쓰기를 만들지 않는다.
  • EDS page-header-lg의 정적·disclosure 변형은 제목 64px, 맥락 72px 최소 높이와 같은 섹션별 12px block padding을 공유한다. 모바일에서 글로벌 내비게이션·제목·맥락의 inline anchor는 16px, sm 이상은 24px이며, 로컬 탭 trigger만 tabs-list 보조 inset 4px만큼 더 안쪽에 놓인다.
  • 뒤로가기를 .page-header-headline-group 안에 넣지 않는다. headline의 이전 가로 스크롤 위치가 복원되어 뒤로가기와 제목 시작점이 화면마다 달라지는 회귀를 막는다.
  • 펼칠 본문이 있는 route만 Reka 기반 DisclosurePageHeader를 사용한다. Header는 항상 보이고 Body와 Footer만 함께 접히며, 오른쪽 끝의 독립 trigger가 aria-expanded와 chevron 상태를 소유한다. 실제 메뉴가 없는 more_horiz placeholder는 금지한다.
  • 청구 계약·유닛·멤버 명세의 card-body는 조회조건·액션·표·페이지네이션을 서로 다른 주요 기능 영역으로 보고 gap-y-4(16px)로 합성한다. toolbar 내부의 control/sub-group은 기존 gap-1·gap-2를 유지하며, 두 계층의 간격을 하나의 부모 gap으로 축약하지 않는다.
  • 부과산정 합계가 0원이어도 자동 Dialog를 띄우지 않는다. 사용자는 편집표에서 바로 입력하거나 명시적 회계 비용 가져오기 액션을 선택한다.
  • Transaction surface의 layout owner는 영역마다 하나다. MainOrg는 card anatomy와 주요 영역 순서·간격만 소유하고 CriteriaToolBarOrg·DataToolBarOrg·PaginationToolBarOrg는 각각 자신의 root에서 width·flex·가로 overflow와 shrink-0을 소유한다. 가로 스크롤 루트는 flex item의 기본 축소로 28px control을 22px까지 누르면 안 된다. Search·Criteria·Action·Pagination은 viewport와 관계없이 각각 한 줄이며 flex-wrap, 반응형 flex-col, 필수 control 숨김을 사용하지 않는다. 청구 명세 Criteria의 세금 태그와 날짜·확인 control도 같은 가로 scroll row에 놓인다. paginated transaction surface는 content-flow가 기본이므로 page만 세로 scroll을 소유하고 card·card-body·table block은 h-full·grow·min-h-0·세로 overflow-auto로 viewport를 채우지 않는다. table block은 가로 overflow만 소유한다. 단일 컴포넌트를 w-full로만 감싸는 pass-through div와 table block 바깥의 중복 overflow wrapper는 금지한다. max-width 정렬, 선택 상태 분기, card header/body/footer, overlay placement처럼 별도 책임이 있는 wrapper는 유지한다. 정본은 leysys-design TRANSACTION-SURFACE-LAYOUT-OWNERSHIP-2026-07-28.md다.
  • 회차 단계 본문은 같은 공용 헤더가 StageWidgetOrg를 한 번만 소유한다. route adapter는 active-group, show-stage, 상태 badge와 필요한 slot만 전달한다.
  • 2026-07-27 L5 검증: 360×800, 412×915, 448×998, 768×1024, 960×900, 1024×768, 1280×800, 1440×900, 1920×1080에서 page horizontal overflow 0, 제목 section 64px, leading/action 각 40px, 고정 영역 겹침과 headline overflow 0을 확인했다. 412×915에서 static overview와 charging·invoicing disclosure의 제목 section은 모두 64px이고, disclosure 본문은 닫힘 기본 → 펼침 → 재닫힘 상태를 유지한다.
  • Chromium/WebKit에서는 EDS가 전역 Native scrollbar에 6px pseudo-element 목표값을 제공한다. scrollbar-width:thin이 우선하는 환경과 Firefox, iOS처럼 skinning을 무시하는 플랫폼은 OS geometry와 자동 숨김 수명을 유지한다.
  • Native의 overlay/reserved 여부는 플랫폼 설정이 소유한다. 반드시 block-size 증가 0이어야 하는 bounded component만 기본 scroll-area-overlay-6px를 선택한다. 2px·4px Overlay는 compact 근거가 있을 때만 사용한다. Overlay는 공통 subtle alpha 0.8 thumb·pointer-events:none이며 일시 표시 기본은 type="scroll", 지속 표시가 업무 계약일 때만 type="auto"다. 별도 variant가 없는 일반 overflow-auto도 같은 thumb token을 소비한다.
  • table wrapper는 tabindex="0", role="region", aria-label="관리비 정산 목록"을 유지한다. 페이지네이션은 wrapper 밖의 다음 형제로 유지해 표를 좌우로 이동해도 현재 범위와 페이지 탐색은 움직이지 않는다.
  • 제품 레포에 scrollbar CSS, <style>, !important, ::-webkit-scrollbar override를 추가하지 않는다.

관련 회귀:

  • composables/__tests__/useBillingPeriodListQuery.spec.js: 기본 12개월, 최대 24개월, 검색·페이지 이동
  • actual/blocks/__tests__/CriteriaToolBarOrg.spec.js: Criteria 범위·폭·초과 범위 Alert Dialog
  • actual/blocks/__tests__/PaginationToolBarOrg.spec.js: compact footer, 중복 visible copy 제거, EDS pagination anatomy
  • actual/blocks/__tests__/DynamicTableOrg.spec.js: 핵심 데이터용 auto/gutter 구성, 접근 가능한 Viewport, pagination 외부 유지
  • service-charge/__tests__/TransactionSurfaceAnatomy.spec.js: pass-through wrapper 0, 모든 관리비 toolbar의 single-line horizontal scroll, 거래 surface의 page-only vertical scroll owner

정기 관리비 목록 금액 컬럼

components/service-charge/actual/blocks/DynamicTableOrg.vue는 월별 업무 흐름을 한 행에서 읽도록 다음 순서로 표시한다.

text
정산명 → 정산기간 → 납부기한
→ 부과액 → 수납액 → 미수잔액 → 수납률
→ 부과 상태 → 수납 상태 → 이월 상태
  • 회차·연도·월·시작일·종료일의 중복 열은 정산명 + 정산기간으로 압축했다.
  • 금액·비율 셀은 text-right tabular-nums, 수납액·미수잔액은 font-medium으로 읽기 쉽게 한다.
  • 목록 수납액(settledAmount)현금수납액(cashCollectedAmount) + 선수금 충당액(advanceAppliedAmount)이다. 선수금 충당은 새 현금 유입이 아니지만 해당 부과를 소멸시키므로 이 목록의 수납 실적에 포함한다.
  • 수납률 = 수납액 / 부과액이며 0분모는 , 최댓값은 100%다. 현금과 선수금의 구성은 상세 화면에서 분리한다.
  • 헤더는 TermTipglossary.js를 사용한다. title은 마우스 호버 설명을, PopoverPortal은 키보드·터치 설명과 표 내부 clipping 회피를 담당한다.
  • 새 CSS, scoped override, 디자인 토큰은 없다. EDS table과 Tailwind 정렬·최소폭 유틸리티만 사용한다.

금액 조립은 useBillingPeriodListMetrics.js가 소유한다.

  • open 차수: useForwarding.forwardingByContract() 결과를 한 번 순회해 당기 부과·미수잔액을 집계하고, 입금 원장의 appliedBreakdown에서 당기 현금수납만 합산한다. 계약별 선수금은 이전 차수 미수에 먼저 충당한 뒤 당기에 귀속될 금액만 advanceAppliedAmount로 더해 settledAmountcollectionRate를 파생한다.
  • 목록 경로에서 useChargeSummaryTable.columnTotals()·useInvoice.invoiceFor()·호실별 useCollectingDetail.detailFor()를 호출하지 않는다. 목록 총계를 위해 466개 호실의 고지서 상세 VM을 생성하면 메인 스레드를 수십 초 점유한다.
  • closed 차수: closedSummary의 정식 camelCase 원천 필드에서 같은 목록 지표를 파생한다.
  • 레거시 closed 차수: 부과총액 → chargeAmount, 수납총액 → cashCollectedAmount, 이월총액 → receivableBalance만 호환한다. 선수금 충당액을 알 수 없으면 합성 수납액·수납률은 null로 둔다.

useFirstChargeReadiness()는 모듈 싱글톤 read model이다. Header·Main·하위 화면이 여러 번 호출해도 Office·차수·catalog·roster를 동기화하는 flush: 'sync' watcher는 하나만 존재해야 한다. 목록 Main은 ready·blockers를 계산해 동적 표와 생성 시트에 props로 전달한다. syncOfficeCharges()의 동일 차수 target count 보정도 값이 실제 바뀔 때 한 번의 배열 교체만 허용하며, 항목별 mutation으로 deep persistence watcher를 반복 깨우지 않는다.

usePeriodClose.closeGate()closePeriod()는 마감 당시 원천 구성 필드와 settledAmount·collectionRateclosedSummary에 고정한다. 기존 한국어 요약 키는 과거 소비처 호환을 위해 유지한다.

테스트:

파일계약
actual/blocks/__tests__/DynamicTableOrg.spec.js네 지표 순서, TermTip, 중복 기간 열 제거
common/__tests__/TermTip.spec.js호버 title과 키보드·터치 Popover 병행
composables/__tests__/useBillingPeriodListMetrics.spec.js현금+선수금 합성, 100% 상한, 레거시 null, 466호실 500ms 상한
composables/__tests__/useFirstChargeReadiness.spec.js여러 소비처가 동일 준비상태·동기 watcher를 공유
composables/__tests__/useChargesDraftPersistence.spec.jstarget count 보정이 불필요한 로컬 저장을 만들지 않음
composables/__tests__/usePeriodClose.spec.js마감 스냅샷 다섯 필드 고정

테스트

파일계약
authSession.spec.js런타임 실패에도 세션과 상품 진입 유지
serviceChargeUnitCatalogSync.spec.js초기 실패 오류 보존, 같은 화면 재시도 성공
EntryAvailability.spec.js경고·권한 메시지·재시도 후 경고 제거 렌더링

확장 주의

새 상품 bootstrap을 추가할 때 데이터 hydration 실패를 인증 리다이렉트로 바꾸지 않는다. 상품마다 idle/loading/available/degraded/error 상태와 재시도 UI를 소유하고, 플랫폼 인증 코어와 선택적 런타임 연결을 느슨하게 유지한다.

반응형 조회기간 criteria

정기 관리비의 기간 criteria는 SSOT MonthRangePicker를 사용한다. native input[type=month] 두 개를 한 행에서 축소하는 방식은 브라우저별 indicator 폭 때문에 모바일에서 충돌하므로 사용하지 않는다.

구성 계약:

  • 바깥 criteria: 전 viewport에서 오른쪽 정렬한 단일 복합 컨트롤
  • 선택값이 의미를 충분히 전달하므로 별도 조회기간 visible label은 두지 않는다.
  • MonthRangePicker와 직접 실행 조회 사이: 2px
  • 모든 viewport: trigger picker-trigger-month-md max-w-full(200px, 8px 스케일), 조회 버튼 shrink-0
  • 선택 상한: 완성된 범위를 validateBillingMonthRange로 검사한다. 24개월 초과 시 Alert Dialog를 열고 마지막 정상 범위를 복원한다.
  • 상시 보조문구와 InfoHint는 두지 않는다.

실측 기준 trigger 내용은 calendar icon 12px + gap 6px + 한국어 범위 약 138px + 좌우 padding 20px으로 약 176px이다. EDS 1.1.4부터 MonthPicker/MonthRangePicker는 --month-picker-width-sm/md/lg(168/200/232px)와 대응하는 picker-trigger-month-sm/md/lg를 제공한다. 관리비 목록은 md 200px을 사용해 trigger와 popover의 외곽선을 정확히 맞춘다. 제품 로컬 width CSS나 임의 폭 유틸리티는 추가하지 않는다.

@leysys/eds@1.1.4에서 월 그리드 셀은 sm 32×32px, md 40×40px, lg 48×48px의 8px 정사각형 스케일을 유지하고, 월 선택 popover 외곽 폭은 sm 168px, md 200px, lg 232px로 정렬한다. 4자리 연도를 표시하는 YearPicker 셀은 기존 직사각형 규격을 유지한다. 이 변경은 EDS 토큰과 picker.css가 소유하며 제품에 로컬 width·height CSS를 추가하지 않는다. Vue wrapper API와 정본 파일에는 변경이 없으므로 제품의 byte-identical wrapper도 수정하지 않는다.

@leysys/eds@1.1.10 소비 시점의 시간 선택기 간격 계약은 다음과 같다. 시간·날짜 입력 세그먼트 내부는 2px, 연속된 일 달력은 column-gap: 0·row-gap: 0, 독립 선택 셀인 월·연도 그리드는 column-gap: 4px·row-gap: 4px이다. Reka가 삽입하는 tbody 경계에서도 월·연도 행 간격이 4px을 유지하며, 정기 관리비 MonthRangePicker의 실제 가로·세로 행 간격도 4px이다. 최신 exact package는 Overlay/Native × 2px/4px/6px scrollbar style matrix도 소유하며, 이 화면은 헤더와 표에 class 없는 전역 기본 Native를 사용한다. 제품 로컬 CSS·wrapper 변형은 없다.

정본 순서:

  1. leysys-design/src/registry/vue/month-range-picker/
  2. leysys-design/src/components/pattern-app/criteria-tool-bar/
  3. 제품 copy src/registry/vue/month-range-picker/
  4. 페이지 components/service-charge/actual/blocks/CriteriaToolBarOrg.vue

또한 requireAuthenticatedRoutehydrateProductRuntime을 background로 시작한다. product hydration을 다시 await하면 느린 RPC가 전체 라우팅을 navigation-pending 상태로 만들므로 금지한다.

첫 화면 반응성과 background 동기화

authSession.js는 같은 사용자·Company로 검증돼 저장된 account context가 있으면 화면을 먼저 연다. Supabase getUserhydrateAuthenticatedAccount는 background에서 재검증한다. 저장 principal의 userId, companyId와 account company가 모두 일치하지 않으면 cache fast path를 쓰지 않고 기존 서버 검증 경로를 유지한다.

서버 검증이 끝난 같은 SPA의 후속 이동은 verifiedAccessToken·verifiedUserId·ready account state와 현재 principal의 userId가 모두 일치할 때 getSession()을 다시 호출하지 않는다. resolveAuthenticatedRoute()가 Company 전용·preview·상품 entitlement 판정과 runtime scheduling을 그대로 수행한다. 새로고침·새 탭·principal 불일치에는 이 경로를 사용하지 않으며 기존 서버 검증으로 돌아간다.

Workspace 홈은 브라우저의 첫 requestIdleCallback에서 preloadRoute(router, '/service-charge/actual')를 실행한다. idle 예약에는 강제 timeout을 두지 않으며, API가 없는 환경만 2048ms fallback timer를 사용한다. focus·pointerenter·touchstart에서도 같은 경로 promise를 재사용해 관리비 route chunk를 미리 준비한다. routePreload.jsrouter.resolve(path).matched에 이미 선언된 component loader만 호출하므로 platform-core에서 상품 .vue를 직접 import하지 않는다. 실패한 preload는 cache에서 제거해 실제 클릭이나 다음 의도 신호에서 재시도할 수 있어야 한다. preload는 Vue 화면 파일만 준비하며 관리비 API를 호출하지 않는다.

상품 runtime은 라우트 반환 뒤 브라우저의 requestIdleCallback에서 시작한다. idle 예약에는 강제 timeout을 두지 않으며, API가 없는 테스트·구형 환경만 2048ms fallback timer를 사용한다. 관리비의 각 원격 hydration 사이에서도 새 idle slot을 기다린다. 백그라운드 작업이 상단 안내 닫기·조회기간 선택 등 사용자 입력보다 우선해서는 안 된다. 예약 뒤 다른 상품으로 이동하거나 로그아웃하면 generation을 바꿔 실행을 취소한다.

productRuntimeBootstrap.js의 관리비 초기화 순서는 다음과 같다.

  1. 현재 Office 정산 기간
  2. 현재 Office 부과 snapshot
  3. 현재 Office 콘솔 단계
  4. 호실 catalog sync
  5. 청구 대상 명부 sync

각 단계는 병렬 Promise.all이 아니라 순차 실행하고 단계 사이에서 새 브라우저 idle slot을 기다린다. Office 전환 watcher도 현재 Office ID를 각 hydration에 전달한다.

세 저장소의 hydration 규칙:

  • hydration identity는 principalId:officeId
  • Supabase loadAll(officeId) 사용
  • 현재 Office 원격 결과만 교체하고 다른 Office cache 보존
  • 원격 state 적용 중 persistence watcher guard
  • nextTick까지 guard를 유지해 hydration 자체가 원격 저장을 일으키지 않음
  • 콘솔 단계 레코드는 반복 clone-replace하지 않고 비반응성 임시 객체에서 병합한 뒤 stateworkflowVersions를 각각 한 번만 교체

청구 대상 설정 시트는 목록과 함께 상시 mount하지 않는다. MainOrg.vue청구 대상 설정 요청을 받았을 때만 v-if로 mount하고, dialog의 close 뒤 unmount한다. 시트가 닫혀 있는 동안 후보 option DOM은 0개여야 한다.

시트가 열린 뒤에도 모든 유닛의 select에 전체 후보를 복제하지 않는다. 비활성 select는 현재 선택값 하나만 유지하고, focus 또는 pointerdown으로 조작 중인 select 하나에만 전체 후보를 render한다. 유닛 수를 U, 후보 수를 C라 할 때 DOM 규모는 O(U × C)가 아니라 O(U + C)여야 한다. 466 유닛·466 후보를 상시 복제해 약 217,000개의 option을 만드는 구현은 금지한다.

회귀 테스트:

파일계약
authSession.spec.js저장 account fast path, 검증 완료 후속 이동의 session 무대기, pending 서버 재검증과 무관한 route 반환, 예약 runtime 취소
HomePag.spec.js유휴 시간 및 focus·pointerenter·touchstart 관리비 entry preload
routePreload.spec.jsrouter 선언 loader 재사용, 경로별 단일 실행, 상품 직접 import 금지
billingRepo.spec.js현재 Office query filter
chargeSnapshotRepo.spec.js현재 Office query filter
consoleStageRepo.spec.js현재 Office query filter
useConsoleStage.spec.js1,000개 원격 단계 레코드의 일괄 병합과 Office 범위 교체
useFirstChargeReadiness.spec.js466 유닛·466 대상 첫 화면 계산 1초 미만
SubjectRosterUi.spec.js시트 lazy mount·close 후 unmount, 현재 조작 중인 select에만 후보 option render

페이지나 auth guard에서 다시 다섯 hydration을 병렬화하거나 원격 hydrate를 일반 사용자 mutation과 같은 watcher 경로로 보내지 않는다.