'use client'; import { useEffect, useState, useCallback, useRef } from 'react'; import { useDashboardStore } from '@/stores/dashboardStore'; import { getDashboardList, getDashboardCards, updateDashboard, deleteDashboard, insertDashboardCard, updateCardPositionsBatch, } from '@/lib/api/dashMenu'; import { DashboardSidebar } from './DashboardSidebar'; import { DashboardCanvas } from './DashboardCanvas'; import { TemplateLibraryModal } from './TemplateLibraryModal'; import { CardSettingsPanel } from './CardSettingsPanel'; import { ControlMode } from '@/components/control/ControlMode'; // ControlPalette 는 ControlMode 의 IDE LeftRail 안에서만 사용됨 (외부 사이드바 교체 폐기) import { useControlMode } from '@/components/control/hooks/useControlMode'; import { useMenu } from '@/contexts/MenuContext'; import { toast } from 'sonner'; import '@/styles/dashboard.css'; interface DashboardLayoutProps { /** 단일 대시보드 모드: AppLayout 사이드바 메뉴가 대시보드 목록 역할을 하므로 * 자체 DashboardSidebar 를 숨기고 지정된 dashboardId 만 로드한다. */ dashboardId?: string; } export function DashboardLayout({ dashboardId: singleDashboardId }: DashboardLayoutProps = {}) { const { dashboards, activeDashboardId, cards, editMode, setDashboards, setActiveDashboard, setCards, addCard, setEditMode, openCreate, libOpen, openLib, closeLib, } = useDashboardStore(); const controlActive = useControlMode((s) => s.active); // controlMode 는 ControlMode 내부에서만 참조 (외부 사이드바 분기 폐기) const { refreshMenus } = useMenu(); const isSingleMode = !!singleDashboardId; const [settingsCardId, setSettingsCardId] = useState(null); const [initialized, setInitialized] = useState(false); const canvasRef = useRef(null); // 대시보드 목록 로드 const loadDashboards = useCallback(async () => { try { const result = await getDashboardList(); const list: Record[] = result?.list ?? []; setDashboards(list); // 단일 모드에서는 URL 의 dashboardId 를 활성화 if (isSingleMode && singleDashboardId) { setActiveDashboard(singleDashboardId); } else if (list.length > 0 && !activeDashboardId) { // 멀티 모드에서는 첫 번째 대시보드 자동 선택 const firstId = list[0].objid ?? list[0].OBJID ?? list[0].dashboard_id ?? list[0].DASHBOARD_ID; setActiveDashboard(firstId); } setInitialized(true); } catch (err) { console.error('[Dashboard] Load failed:', err); setInitialized(true); } }, [setDashboards, setActiveDashboard, activeDashboardId, isSingleMode, singleDashboardId]); useEffect(() => { loadDashboards(); }, []); // 단일 모드에서 URL id 가 바뀌면 활성 전환 useEffect(() => { if (isSingleMode && singleDashboardId && singleDashboardId !== activeDashboardId) { setActiveDashboard(singleDashboardId); } }, [isSingleMode, singleDashboardId, activeDashboardId, setActiveDashboard]); // 대시보드 전환 시 카드 로드 // stale guard: 응답 도착 시 이미 다른 대시보드로 전환됐으면 무시 (race 방지) const loadCards = useCallback(async (dashId: string) => { try { const cardList = await getDashboardCards(dashId); if (useDashboardStore.getState().activeDashboardId === dashId) { const normalized = (cardList ?? []).map((c: Record) => ({ ...c, position_x: c.position_x ?? c.POSITION_X, position_y: c.position_y ?? c.POSITION_Y, width: c.width ?? c.WIDTH, height: c.height ?? c.HEIGHT, is_collapsed: c.is_collapsed ?? c.IS_COLLAPSED ?? false, })); setCards(normalized); } } catch (err) { console.error('[Dashboard] Load cards failed:', err); if (useDashboardStore.getState().activeDashboardId === dashId) { setCards([]); } } }, [setCards]); useEffect(() => { if (activeDashboardId) { loadCards(activeDashboardId); setEditMode(false); } }, [activeDashboardId, loadCards, setEditMode]); // 활성 대시보드 정보 const dashKey = (d: Record) => d.objid ?? d.OBJID ?? d.dashboard_id ?? d.DASHBOARD_ID; const activeDash = dashboards.find((d) => dashKey(d) === activeDashboardId); const dashName = activeDash?.name ?? activeDash?.NAME ?? '대시보드'; // 대시보드 CRUD — 생성 모달은 AppLayout(전역 헤더) 이 소유. 여기서는 모달만 열어준다. const handleAddDashboard = () => openCreate(); const handleRenameDashboard = async (id: string) => { const dash = dashboards.find((d) => dashKey(d) === id); if (!dash) return; const newName = prompt('새 이름:', dash.name ?? dash.NAME ?? ''); if (!newName?.trim()) return; try { await updateDashboard(id, { name: newName.trim() }); await loadDashboards(); try { await refreshMenus(); } catch { /* refresh 실패 무시 */ } toast.success('이름 변경됨'); } catch (err) { toast.error('이름 변경 실패'); } }; const handleDeleteDashboard = async (id: string) => { if (!isSingleMode && dashboards.length <= 1) { toast.warning('마지막 대시보드는 삭제할 수 없습니다'); return; } const dash = dashboards.find((d) => dashKey(d) === id); if (!confirm(`"${dash?.name ?? dash?.NAME}" 을 삭제합니다.`)) return; try { await deleteDashboard(id); await loadDashboards(); try { await refreshMenus(); } catch { /* refresh 실패 무시 */ } toast.info('대시보드 삭제됨'); } catch (err) { toast.error('삭제 실패'); } }; const handleSwitchDashboard = (id: string) => { if (id === activeDashboardId) return; setActiveDashboard(id); }; // 템플릿 추가 (라이브러리 → 카드) — 화면 중앙 배치 + 기존 카드 수만큼 stagger // 저장 단위: % (0~100). 캔버스 크기 무관하게 반응형으로 동작. const handleSelectTemplate = async (template: Record) => { if (!activeDashboardId) return; const templateId = template.template_id ?? template.TEMPLATE_ID; try { // 기본 크기: 너비 45%, 높이 50%. stagger: 3% 씩 오프셋 (최대 8장 순환) const w = 45; const h = 50; const stagger = (cards.length % 8) * 3; const x = Math.max(2, Math.min(100 - w - 2, Math.round((100 - w) / 2) + stagger)); const y = Math.max(2, Math.min(100 - h - 2, Math.round((100 - h) / 2) + stagger)); const result = await insertDashboardCard(activeDashboardId, { template_id: templateId, position_x: x, position_y: y, width: w, height: h, }); addCard({ ...result, template_id: templateId, template_name: template.name ?? template.NAME, template_category: template.category ?? template.CATEGORY, primary_table: template.primary_table ?? template.PRIMARY_TABLE, position_x: x, position_y: y, width: w, height: h, is_collapsed: false, }); closeLib(); if (!editMode) setEditMode(true); toast.success(`${template.name ?? template.NAME} 카드를 추가했습니다`); } catch (err) { toast.error('카드 추가 실패'); } }; // 레이아웃 저장 const handleSaveLayout = useCallback(async () => { if (!activeDashboardId) return; try { const cardPositions = cards.map((c) => ({ card_id: c.card_id ?? c.CARD_ID, position_x: c.position_x ?? c.POSITION_X ?? 0, position_y: c.position_y ?? c.POSITION_Y ?? 0, width: c.width ?? c.WIDTH ?? 600, height: c.height ?? c.HEIGHT ?? 400, is_collapsed: c.is_collapsed ?? c.IS_COLLAPSED ?? false, })); await updateCardPositionsBatch(activeDashboardId, cardPositions); toast.success(`${cards.length}개 카드 레이아웃 저장됨`); } catch (err) { toast.error('저장 실패'); } }, [activeDashboardId, cards]); // 헤더/FAB 가 dispatchEvent('dash:save') 로 저장 요청 → 여기서 수신해 실행 useEffect(() => { const onSaveReq = () => { handleSaveLayout(); }; window.addEventListener('dash:save', onSaveReq); return () => window.removeEventListener('dash:save', onSaveReq); }, [handleSaveLayout]); // 설정 카드 정보 const settingsCard = settingsCardId ? cards.find((c) => (c.card_id ?? c.CARD_ID) === settingsCardId) : null; if (!initialized) { return (
로딩 중...
); } return (
{/* 사이드바 — 단일 모드에선 AppLayout 메뉴가 대시보드 목록 역할이므로 자체 사이드바 숨김. 제어 모드 takeover 는 ControlMode 의 IDE LeftRail 이 담당 (v3 V3Takeover) — 외부 사이드바 교체 X */} {!isSingleMode && !controlActive ? ( ) : null} {/* 제어 모드 ON 이지만 카드 미선택 상태에서는 사이드바 자체를 숨김 (IDE 가 화면 takeover 할 자리 확보) */}
{activeDashboardId ? ( <> {/* 편집/제어 툴바는 이제 헤더로 hoist. 캔버스 FAB 이 모드 내 액션 담당. */} {/* ★ flex container로 만들어야 안쪽 dash-canvas가 flex:1로 늘어남 */}
openLib()} onOpenSettings={(id) => setSettingsCardId(id)} controlMode={controlActive} /> {settingsCard && !controlActive && ( setSettingsCardId(null)} /> )}
) : (
📋
대시보드가 없습니다
)}
closeLib()} onSelectTemplate={handleSelectTemplate} />
); }