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:
@@ -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({
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = {
|
||||
// 새 오늘의 이슈 (리스트 형태)
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
|
||||
// 요약 데이터 계산
|
||||
|
||||
Reference in New Issue
Block a user