다크모드
FE 메인테이너 — service-charge 사이드바 그룹 모델 + CoA 공유 인프라
독자: FE 메인테이너/AI. "이 영역을 이어 개발하려면 무엇을 알아야 하나"를 정의한다. 정본 스펙:
docs/superpowers/specs/2026-06-20-billing-menu-ia-coa-consolidation-design.mdBE 계약:docs/handoff/backend/billing-menu-ia-coa-consolidation.mdas-built 기준: 2026-06-20 (브랜치feature/billing-menu-ia-coa-consolidation, main 머지 예정)
0. Workspace 메뉴 표시명 (2026-07-16)
Workspace 전역 업무 메뉴와 홈 업무 카드는 src/composables/workspaceNav.js가 PRODUCT_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또는CriteriaToolBarOrg에activePreset(공동주택·집합건물) 텍스트 표시
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' 팩으로 매핑한다. 팩 분할 시:
src/data/billingPresets.js에PRESET_PACK수정 ('공동주택': 'service-charge-cho','집합건물': 'service-charge-jip')PACKS객체에 분리 팩 추가 (SERVICE_CHARGE_CHO_ACCOUNTS,SERVICE_CHARGE_JIP_ACCOUNTS)useChartOfAccounts무변경(팩 코드만 달라짐)
7-2. 엄격 도메인 필터 (후속)
팩 분할 후, account-setting 화면에서 활성 프리셋 계정만 표시(이종 도메인 계정 숨김). DynamicTableOrg에서 accounts를 activePreset 기반으로 추가 필터.
7-3. 프리셋 선택 UI (후속)
현재 localStorage(leyve.billing.preset)와 데모 시드(공동주택)로만 프리셋을 전환한다. 워크스페이스 설정 화면에 셀렉트 UI 추가 → useBillingPreset.setPreset(preset) 호출.
7-4. useBillingPreset 네이밍 리네임 (후속)
useBillingPreset → useWorkspacePreset, leyve.billing.preset → leyve.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 유지 확인accountingEnabledOFF에서 런처 계정과목 도달 가능 + 그 외 회계 메뉴 비노출- lease/sales/purchasing 그룹 사이드바 회귀 0
- console(TypeError/Vue warn) 0