Skip to content

FE 메인테이너 — service-charge 사이드바 그룹 모델 + CoA 공유 인프라

독자: FE 메인테이너/AI. "이 영역을 이어 개발하려면 무엇을 알아야 하나"를 정의한다. 정본 스펙: docs/superpowers/specs/2026-06-20-billing-menu-ia-coa-consolidation-design.md BE 계약: docs/handoff/backend/billing-menu-ia-coa-consolidation.md as-built 기준: 2026-06-20 (브랜치 feature/billing-menu-ia-coa-consolidation, main 머지 예정)


0. Workspace 메뉴 표시명 (2026-07-16)

Workspace 전역 업무 메뉴와 홈 업무 카드는 src/composables/workspaceNav.jsPRODUCT_MODULES를 변환해 만든다. service-charge는 상품·결제 화면에서 관리비, 업무 메뉴와 홈 업무 카드에서 관리비관리로 표시한다.

  • 상품 표시명: PRODUCT_MODULES[].name
  • 업무 메뉴 표시명: PRODUCT_MODULES[].menuLabel(없으면 name 폴백)
  • 홈 업무 카드 표시명: WORKSPACE_APP_ITEMS가 업무 메뉴와 같은 label을 사용
  • 회귀 테스트: src/composables/__tests__/workspaceNav.spec.js

페이지나 AppAsideOrg.vue에서 문자열을 직접 덮어쓰지 않는다. 다른 상품도 상품명과 메뉴명이 달라져야 할 때 같은 menuLabel 계약을 사용한다.


1. buildLineRoutes — lineLevel 슬롯

기존 구조 (에디션 전용)

buildLineRoutes(line, manifest, descriptor)는 에디션 구동 전용이었다. 에디션별 콘솔({root}/actual, {root}/provisional 등)을 app-view 자식으로 등록.

신규 슬롯 — lineLevel (비에디션 공유 라우트)

관리비설정·퀵검침은 actual/provisional 에디션 공유 = 라인레벨. LINE_MANIFEST[line].lineLevel에 비에디션 세그먼트→뷰 매핑을 선언하면 팩토리가 {root}/{seg} 경로로 라우트를 생성한다.

js
// LINE_MANIFEST['service-charge'].lineLevel (as-built)
{
  'setting/general':                () => import('…/service-charge/setting/general/IndexView.vue'),
  'setting/surcharge':              () => import('…/service-charge/setting/surcharge/IndexView.vue'),
  'setting/collecting':             () => import('…/service-charge/setting/collecting/IndexView.vue'),
  'setting/item/general':           () => import('…/service-charge/setting/item/general/IndexView.vue'),
  'setting/item/group':             () => import('…/service-charge/setting/item/group/IndexView.vue'),
  'setting/initial-data/general':   () => import('…/service-charge/setting/initial-data/general/IndexView.vue'),
  'setting/initial-data/group':     () => import('…/service-charge/setting/initial-data/group/IndexView.vue'),
  'setting/account/general':        () => import('…/service-charge/setting/account/general/IndexView.vue'),
  'metering/general':               () => import('…/service-charge/metering/general/IndexView.vue'),
}
  • 생성되는 라우트 경로: /service-charge/{seg} (예: /service-charge/setting/general)
  • meta.billing = 'service-charge' (에디션 없음)
  • 에디션 콘솔(app-view 자식)과 동일 RouteLayout(LineLayoutView) 내에 공존

팩토리 확장 규칙

lineLevel을 추가하는 다른 라인(예: 미래 lease 라인의 라인레벨 설정)도 동일 패턴으로 선언한다. 팩토리는 manifest.lineLevel이 있으면 순회, 없으면 스킵.


2. descriptor.sidebar — 그룹 모델

진화 전 (flat)

js
sidebar: [
  { labelKey: "billing.editions.actual", path: "/service-charge/actual" },
  { labelKey: "billing.editions.provisional", path: "/service-charge/provisional" },
];

진화 후 (그룹 구조, as-built)

js
sidebar: [
  {
    groupKey: "serviceCharge",
    labelKey: "billing.lines.serviceCharge",
    items: [
      { labelKey: "billing.editions.actual", path: "/service-charge/actual" },
      { labelKey: "billing.editions.provisional", path: "/service-charge/provisional" },
    ],
  },
  {
    groupKey: "task",
    kind: "direct",
    labelKey: "billing.setting.metering",
    icon: "speed",
    path: "/service-charge/metering/general",
  },
  {
    groupKey: "setting",
    labelKey: "billing.groups.setting",
    items: [
      { labelKey: "billing.setting.general", path: "/service-charge/setting/general" },
      { labelKey: "billing.setting.item", path: "/service-charge/setting/item/general" },
      {
        labelKey: "billing.setting.initialData",
        path: "/service-charge/setting/initial-data/general",
      },
      { labelKey: "billing.setting.collecting", path: "/service-charge/setting/collecting" },
      { labelKey: "billing.setting.surcharge", path: "/service-charge/setting/surcharge" },
      { labelKey: "billing.setting.account", path: "/service-charge/setting/account/general" },
    ],
  },
];
  • groupKey: 그룹 고유 식별자(i18n 미사용, 내부 키)
  • labelKey: i18n 그룹 제목 (ko/en 양방향 필요)
  • kind: 'direct': 자식이 하나뿐인 disclosure를 만들지 않고 검침처럼 직속 메뉴로 렌더한다.
  • items[].path: 라우터 이동 값. 회계 연결도 service-charge 로컬 경로를 사용해 현재 셸을 유지한다.
  • lease/sales/purchasing/hospitality의 flat sidebar는 단일 groupKey: 'console' 그룹으로 무손실 마이그레이션됨

3. LineAsideOrg — 그룹 v-for 렌더

src/components/billing/_core/LineAsideOrg.vue

변경점

  • 이전: items flat v-for
  • 이후: 그룹 v-for(외) + items v-for(내)
vue
<!-- LineAsideOrg.vue (개략) -->
<template v-for="group in descriptor.sidebar" :key="group.groupKey">
  <p class="sidebar-menu-group-label">{{ t(group.labelKey) }}</p>
  <ul class="sidebar-menu">
    <li v-for="item in group.items" :key="item.path">
      <RouterLink :to="item.path" class="sidebar-menu-item">
        {{ t(item.labelKey) }}
      </RouterLink>
    </li>
  </ul>
</template>
  • DS 클래스: sidebar-menu-group-label · sidebar-menu · sidebar-menu-item-parent / sidebar-menu-item-child (정본: leysys-design)
  • 회계 연결은 설정 그룹 안의 /service-charge/setting/account/general로 이동한다. 회계 원장을 관리비의 계정과목으로 오인시키는 별도 1항목 그룹을 만들지 않는다.

4. account-setting — 프리셋 인지 소비 컴포저블

src/components/accounting/.../basic-setting/account/general/blocks/DynamicTableOrg.vue

소비하는 컴포저블

컴포저블역할
useBillingPreset()activePreset(공동주택/집합건물/null) 읽기
useChartOfAccounts(activePacks)활성 팩 계정 목록 파생

데이터 흐름

js
// DynamicTableOrg.vue (개략)
import { useBillingPreset } from "@/composables/useBillingPreset";
import { useChartOfAccounts } from "@/composables/useChartOfAccounts";
import { PRESET_PACK } from "@/data/billingPresets";

const { activePreset } = useBillingPreset();
const activePacks = computed(() => (activePreset.value ? [PRESET_PACK[activePreset.value]] : []));
const accounts = computed(() => useChartOfAccounts(activePacks.value));
  • PRESET_PACK: { '공동주택': 'service-charge', '집합건물': 'service-charge' } (as-built 단일 팩)
  • 프리셋 미선택(null): activePacks = []useChartOfAccounts([]) → 코어(0xxx)만
  • BS/IS 탭: byStatement 필터. account.statementType === 'BS' 또는 'IS'로 분류
  • 도메인 라벨: HeaderOrg 또는 CriteriaToolBarOrgactivePreset(공동주택·집합건물) 텍스트 표시

5. always-open 게이팅 위치

5-1. 워크스페이스 런처 (components/workspace/AppAsideOrg.vue)

  • 변경 전: 회계 앱 그룹 전체 v-if="accountingEnabled" 통짜 숨김
  • 변경 후 (as-built):
    • 회계 앱 그룹 항상 노출
    • 그룹 내 계정과목 항목: ungated (항상 표시)
    • 그 외 서브메뉴: v-if="accountingEnabled" 개별 게이팅
    • dead 스텁(VAT·원천·법인조정·개인조정 서브메뉴 9개) 제거 완료

5-2. 회계앱 내부 사이드바 (components/accounting/AppSideNavMenuOrg.vue)

  • account/general 섹션: ungated (항상 표시·진입 가능)
  • 나머지 섹션(전표·원장·재무제표·마감 등): v-if="accountingEnabled" 게이팅

5-3. 게이팅 소비 컴포저블

js
import { useModuleSubscription } from "@/composables/useModuleSubscription";
const { accountingEnabled } = useModuleSubscription();

6. 관리비설정 파일 이동 (git mv)

이전 전 경로이전 후 경로
views/billing/setting/*views/service-charge/setting/*
components/billing/setting/*components/service-charge/setting/*
  • 이동 트리 내부 import(@/components/billing/setting/*)는 repoint 완료
  • 퀵검침 뷰: views/service-charge/metering/general/IndexView.vue (신규 플레이스홀더 스캐폴드)
  • 빌링 우산 라우터(router/index.js)의 /billing/setting/* 7라우트 제거 완료

7. 확장 포인트 (후속 트랙)

7-1. 도메인 팩 분할 (후속)

현재 PRESET_PACK이 공동주택·집합건물을 모두 단일 'service-charge' 팩으로 매핑한다. 팩 분할 시:

  1. src/data/billingPresets.jsPRESET_PACK 수정 ('공동주택': 'service-charge-cho', '집합건물': 'service-charge-jip')
  2. PACKS 객체에 분리 팩 추가 (SERVICE_CHARGE_CHO_ACCOUNTS, SERVICE_CHARGE_JIP_ACCOUNTS)
  3. useChartOfAccounts 무변경(팩 코드만 달라짐)

7-2. 엄격 도메인 필터 (후속)

팩 분할 후, account-setting 화면에서 활성 프리셋 계정만 표시(이종 도메인 계정 숨김). DynamicTableOrg에서 accountsactivePreset 기반으로 추가 필터.

7-3. 프리셋 선택 UI (후속)

현재 localStorage(leyve.billing.preset)와 데모 시드(공동주택)로만 프리셋을 전환한다. 워크스페이스 설정 화면에 셀렉트 UI 추가 → useBillingPreset.setPreset(preset) 호출.

7-4. useBillingPreset 네이밍 리네임 (후속)

useBillingPresetuseWorkspacePreset, leyve.billing.presetleyve.workspace.preset 리네임. 소비처 일괄 수정(grep useBillingPreset).

7-5. 빌링 우산(/billing) 최종 해체 (후속)

현재 /billing 우산에 hospitality 폴백이 남아 있다. hospitality가 자체 라인으로 추출되면 AppAsideOrg.vue(billing)와 /billing 라우트 전체 제거 가능.

7-6. 퀵검침 실기능 배선 (후속)

views/service-charge/metering/general/IndexView.vue는 현재 플레이스홀더. 검침 입력·조회 기능은 별도 트랙(별도 스펙 필요).


8. 테스트 위치

대상테스트 파일
buildLineRoutes lineLevel 라우트 생성·meta 배선 결정성src/router/__tests__/buildLineRoutes.spec.js
CoA 활성 프리셋 반영(공동주택→9112·집합건물→9113 가시성)src/composables/__tests__/useChartOfAccounts.spec.js
service-charge 사이드바 그룹 렌더src/components/billing/_core/__tests__/LineAsideOrg.spec.js
always-open: accountingEnabled OFF에서 계정과목 도달src/components/workspace/__tests__/AppAsideOrg.spec.js

Playwright 시나리오(수동):

  • service-charge 사이드바에서 개요, 정기 관리비·중간정산, 직속 검침, 설정을 렌더
  • 관리비설정 이전 회귀 0(/service-charge/setting/* 렌더)
  • 검침 스텁 진입(/service-charge/metering/general)
  • 회계 연결 → service-charge 셸 안의 account-setting 안내 진입
  • src/router/__tests__/globalIaIntegrity.spec.js에서 service-charge matched chain 유지 확인
  • accountingEnabled OFF에서 런처 계정과목 도달 가능 + 그 외 회계 메뉴 비노출
  • lease/sales/purchasing 그룹 사이드바 회귀 0
  • console(TypeError/Vue warn) 0