feat: [Phase 1] 모듈 레지스트리 + 라우트 가드

- src/modules/ 모듈 시스템 (types, registry, tenant-config)
- useModules() 훅: 테넌트 industry 기반 모듈 활성화 판단
- ModuleGuard: 비허용 모듈 라우트 접근 차단 (클라이언트 사이드)
- (protected)/layout.tsx에 ModuleGuard 적용
- industry 미설정 테넌트는 가드 비활성 (하위 호환)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-03-18 14:54:42 +09:00
parent 68b1112034
commit 0a65609e5a
6 changed files with 264 additions and 1 deletions

54
src/hooks/useModules.ts Normal file
View File

@@ -0,0 +1,54 @@
'use client';
/**
* 테넌트 모듈 접근 훅
*
* 현재 로그인한 테넌트의 활성 모듈 정보와 헬퍼 함수를 제공.
*
* @example
* const { isEnabled, isRouteAllowed } = useModules();
* if (isEnabled('production')) { ... }
* if (isRouteAllowed('/production/work-orders')) { ... }
*/
import { useMemo } from 'react';
import { useAuthStore } from '@/stores/authStore';
import type { ModuleId } from '@/modules/types';
import { resolveEnabledModules, isModuleEnabled } from '@/modules/tenant-config';
import { getModuleForRoute, getEnabledDashboardSections } from '@/modules';
export function useModules() {
const tenant = useAuthStore((state) => state.currentUser?.tenant);
const enabledModules = useMemo<ModuleId[]>(() => {
if (!tenant) return [];
return resolveEnabledModules({
industry: tenant.options?.industry,
// Phase 2: explicitModules: tenant.options?.modules
});
}, [tenant]);
const isEnabled = useMemo(() => {
return (moduleId: ModuleId) => isModuleEnabled(moduleId, enabledModules);
}, [enabledModules]);
const isRouteAllowed = useMemo(() => {
return (pathname: string) => {
const owningModule = getModuleForRoute(pathname);
if (owningModule === 'common') return true;
return enabledModules.includes(owningModule);
};
}, [enabledModules]);
const dashboardSections = useMemo(() => {
return getEnabledDashboardSections(enabledModules);
}, [enabledModules]);
return {
enabledModules,
isEnabled,
isRouteAllowed,
dashboardSections,
tenantIndustry: tenant?.options?.industry,
};
}