feat: [Phase 2] 대시보드 모듈 디커플링

- MODULE_DEPENDENT_SECTIONS 매핑 + sectionRequiresModule() 헬퍼 추가
- CEODashboard: 비활성 모듈 섹션 필터 + API 호출 스킵
- DashboardSettingsDialog: 비활성 모듈 섹션 설정 숨김
- CalendarSection: 비활성 모듈 링크/필터 옵션 제외
- useSectionSummary: 요약 네비바에서 비활성 모듈 섹션 제외
- 안전장치: tenantIndustry 미설정 시 기존 동작 100% 유지

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-03-18 15:14:59 +09:00
parent 0a65609e5a
commit 4650121416
5 changed files with 96 additions and 16 deletions

View File

@@ -41,17 +41,23 @@ import { getCardManagementModalConfigWithData } from './modalConfigs';
import { transformEntertainmentDetailResponse, transformWelfareDetailResponse, transformVatDetailResponse } from '@/lib/api/dashboard/transformers';
import { toast } from 'sonner';
import { consumeStaleSections, DASHBOARD_INVALIDATE_EVENT, type DashboardSectionKey } from '@/lib/dashboard-invalidation';
import { useModules } from '@/hooks/useModules';
import { sectionRequiresModule } from './types';
export function CEODashboard() {
const router = useRouter();
// API 데이터 Hook
// 모듈 활성화 정보 (tenantIndustry 미설정 시 모든 모듈 표시)
const { isEnabled, tenantIndustry } = useModules();
const moduleAware = !!tenantIndustry; // industry 설정 시에만 모듈 필터링 적용
// API 데이터 Hook (모듈 비활성 시 API 호출 스킵)
const apiData = useCEODashboard({
salesStatus: true,
purchaseStatus: true,
dailyProduction: true,
unshipped: true,
construction: true,
dailyProduction: !moduleAware || isEnabled('production'),
unshipped: true, // 공통 (outbound/logistics)
construction: !moduleAware || isEnabled('construction'),
dailyAttendance: true,
});
@@ -548,8 +554,16 @@ export function CEODashboard() {
}
}, [calendarData]);
// 섹션 순서
const sectionOrder = dashboardSettings.sectionOrder ?? DEFAULT_SECTION_ORDER;
// 섹션 순서 (모듈 비활성 섹션 필터링)
const sectionOrder = useMemo(() => {
const rawOrder = dashboardSettings.sectionOrder ?? DEFAULT_SECTION_ORDER;
if (!moduleAware) return rawOrder; // industry 미설정 시 전부 표시
return rawOrder.filter((key) => {
const requiredModule = sectionRequiresModule(key);
if (!requiredModule) return true; // 공통 섹션
return isEnabled(requiredModule);
});
}, [dashboardSettings.sectionOrder, moduleAware, isEnabled]);
// 요약 네비게이션 바 훅
const { summaries, activeSectionKey, sectionRefs, scrollToSection } = useSectionSummary({

View File

@@ -1,6 +1,6 @@
'use client';
import { useState, useCallback, useEffect } from 'react';
import { useState, useCallback, useEffect, useMemo } from 'react';
import { Button } from '@/components/ui/button';
import {
Dialog,
@@ -19,7 +19,8 @@ import type {
WelfareCalculationType,
SectionKey,
} from '../types';
import { DEFAULT_SECTION_ORDER, SECTION_LABELS } from '../types';
import { DEFAULT_SECTION_ORDER, SECTION_LABELS, sectionRequiresModule } from '../types';
import { useModules } from '@/hooks/useModules';
import {
SectionRow,
StatusBoardItemsList,
@@ -40,6 +41,10 @@ export function DashboardSettingsDialog({
settings,
onSave,
}: DashboardSettingsDialogProps) {
// 모듈 활성화 정보 (industry 미설정 시 전부 표시)
const { isEnabled, tenantIndustry } = useModules();
const moduleAware = !!tenantIndustry;
const [localSettings, setLocalSettings] = useState<DashboardSettings>(settings);
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({
todayIssueList: false,
@@ -53,8 +58,16 @@ export function DashboardSettingsDialog({
const [draggedSection, setDraggedSection] = useState<SectionKey | null>(null);
const [dragOverSection, setDragOverSection] = useState<SectionKey | null>(null);
// 섹션 순서
const sectionOrder = localSettings.sectionOrder ?? DEFAULT_SECTION_ORDER;
// 섹션 순서 (모듈 비활성 섹션 숨김)
const sectionOrder = useMemo<SectionKey[]>(() => {
const rawOrder = localSettings.sectionOrder ?? DEFAULT_SECTION_ORDER;
if (!moduleAware) return rawOrder;
return rawOrder.filter((key: SectionKey) => {
const requiredModule = sectionRequiresModule(key);
if (!requiredModule) return true;
return isEnabled(requiredModule);
});
}, [localSettings.sectionOrder, moduleAware, isEnabled]);
// settings가 변경될 때 로컬 상태 업데이트
useEffect(() => {

View File

@@ -16,6 +16,7 @@ import { ScheduleCalendar } from '@/components/common/ScheduleCalendar';
import type { ScheduleEvent } from '@/components/common/ScheduleCalendar/types';
import { getCalendarEventsForYear, type CalendarEvent } from '@/constants/calendarEvents';
import { useCalendarScheduleStore } from '@/stores/useCalendarScheduleStore';
import { useModules } from '@/hooks/useModules';
import { CollapsibleDashboardCard } from '../components';
import type {
CalendarScheduleItem,
@@ -117,6 +118,12 @@ const TASK_FILTER_OPTIONS: { value: ExtendedTaskFilterType; label: string }[] =
{ value: 'issue', label: '이슈' },
];
// 일정 타입 → 모듈 매핑 (이 타입의 링크/필터가 해당 모듈을 요구)
const SCHEDULE_TYPE_MODULE: Record<string, string> = {
order: 'production',
construction: 'construction',
};
export function CalendarSection({
schedules,
issues = [],
@@ -124,12 +131,24 @@ export function CalendarSection({
onScheduleEdit,
}: CalendarSectionProps) {
const router = useRouter();
const { isEnabled, tenantIndustry } = useModules();
const moduleAware = !!tenantIndustry;
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());
const [currentDate, setCurrentDate] = useState(new Date());
const [, _setViewType] = useState<CalendarViewType>('month');
const [deptFilter, setDeptFilter] = useState<CalendarDeptFilterType>('all');
const [taskFilter, setTaskFilter] = useState<ExtendedTaskFilterType>('all');
// 모듈 기반 업무 필터 옵션 (비활성 모듈 필터 숨김)
const filteredTaskFilterOptions = useMemo(() => {
if (!moduleAware) return TASK_FILTER_OPTIONS;
return TASK_FILTER_OPTIONS.filter((option) => {
const requiredModule = SCHEDULE_TYPE_MODULE[option.value];
if (!requiredModule) return true;
return isEnabled(requiredModule as 'production' | 'construction');
});
}, [moduleAware, isEnabled]);
// 스토어에서 공휴일/세무일정 가져오기 (API 연동)
const schedulesByYear = useCalendarScheduleStore((s) => s.schedulesByYear);
const fetchSchedules = useCalendarScheduleStore((s) => s.fetchSchedules);
@@ -272,7 +291,13 @@ export function CalendarSection({
};
// 일정 타입별 상세 페이지 링크 생성 (bill_123 → /ko/accounting/bills/123)
// 모듈 비활성 시 해당 타입의 링크 숨김
const getScheduleLink = (schedule: CalendarScheduleItem): string | null => {
// 모듈 의존 타입인데 해당 모듈이 비활성이면 링크 없음
const requiredModule = SCHEDULE_TYPE_MODULE[schedule.type];
if (moduleAware && requiredModule && !isEnabled(requiredModule as 'production' | 'construction')) {
return null;
}
const basePath = SCHEDULE_TYPE_ROUTES[schedule.type];
if (!basePath) return null;
// expected_expense는 목록 페이지만 존재 (상세 페이지 없음)
@@ -383,7 +408,7 @@ export function CalendarSection({
<SelectValue />
</SelectTrigger>
<SelectContent>
{TASK_FILTER_OPTIONS.map((option) => (
{filteredTaskFilterOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
@@ -432,7 +457,7 @@ export function CalendarSection({
<SelectValue />
</SelectTrigger>
<SelectContent>
{TASK_FILTER_OPTIONS.map((option) => (
{filteredTaskFilterOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>

View File

@@ -3,6 +3,7 @@
*/
import type React from 'react';
import type { ModuleId } from '@/modules/types';
// 체크포인트 타입 (경고/성공/에러/정보)
export type CheckPointType = 'success' | 'warning' | 'error' | 'info';
@@ -716,6 +717,21 @@ export interface DetailModalConfig {
table?: TableConfig;
}
// ===== 모듈별 섹션 매핑 (Phase 2: Dashboard Decoupling) =====
/** 특정 모듈이 필요한 섹션 매핑 (여기 없는 섹션은 공통 = 항상 표시) */
export const MODULE_DEPENDENT_SECTIONS: Partial<Record<SectionKey, ModuleId>> = {
production: 'production',
shipment: 'production',
construction: 'construction',
// unshipped는 공통(outbound/logistics) — 모듈 의존성 없음
};
/** 섹션이 요구하는 모듈 ID 반환. 공통 섹션이면 null */
export function sectionRequiresModule(sectionKey: SectionKey): ModuleId | null {
return MODULE_DEPENDENT_SECTIONS[sectionKey] ?? null;
}
// 기본 설정값
export const DEFAULT_DASHBOARD_SETTINGS: DashboardSettings = {
// 새 오늘의 이슈 (리스트 형태)

View File

@@ -2,7 +2,8 @@
import { useMemo, useEffect, useState, useRef, useCallback } from 'react';
import type { CEODashboardData, DashboardSettings, SectionKey } from './types';
import { SECTION_LABELS } from './types';
import { SECTION_LABELS, sectionRequiresModule } from './types';
import { useModules } from '@/hooks/useModules';
export type SummaryStatus = 'normal' | 'warning' | 'danger';
@@ -220,10 +221,21 @@ export function useSectionSummary({
// 칩 클릭으로 선택된 키 — 해당 섹션이 화면에 보이는 한 유지
const pinnedKey = useRef<SectionKey | null>(null);
// 활성화된 섹션만 필터
// 모듈 활성화 정보 (tenantIndustry 미설정 시 전부 표시)
const { isEnabled, tenantIndustry } = useModules();
const moduleAware = !!tenantIndustry;
// 활성화된 섹션만 필터 (설정 + 모듈)
const enabledSections = useMemo(
() => sectionOrder.filter((key) => isSectionEnabled(key, dashboardSettings)),
[sectionOrder, dashboardSettings],
() => sectionOrder.filter((key) => {
if (!isSectionEnabled(key, dashboardSettings)) return false;
if (moduleAware) {
const requiredModule = sectionRequiresModule(key);
if (requiredModule && !isEnabled(requiredModule)) return false;
}
return true;
}),
[sectionOrder, dashboardSettings, moduleAware, isEnabled],
);
// 요약 데이터 계산