280 lines
9.4 KiB
TypeScript
280 lines
9.4 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState, useCallback, useRef } from 'react';
|
|
import { useDashboardStore } from '@/stores/dashboardStore';
|
|
import {
|
|
getDashboardList,
|
|
getDashboardCards,
|
|
insertDashboard,
|
|
updateDashboard,
|
|
deleteDashboard,
|
|
insertDashboardCard,
|
|
updateCardPositionsBatch,
|
|
deleteDashboardCard,
|
|
} from '@/lib/api/dashMenu';
|
|
import { DashboardSidebar } from './DashboardSidebar';
|
|
import { DashboardToolbar } from './DashboardToolbar';
|
|
import { DashboardCanvas } from './DashboardCanvas';
|
|
import { TemplateLibraryModal } from './TemplateLibraryModal';
|
|
import { CardSettingsPanel } from './CardSettingsPanel';
|
|
import { ControlMode } from '@/components/control/ControlMode';
|
|
import { ControlPalette } from '@/components/control/ControlPalette';
|
|
import { useControlMode } from '@/components/control/hooks/useControlMode';
|
|
import { toast } from 'sonner';
|
|
import '@/styles/dashboard.css';
|
|
|
|
export function DashboardLayout() {
|
|
const {
|
|
dashboards,
|
|
activeDashboardId,
|
|
cards,
|
|
editMode,
|
|
setDashboards,
|
|
setActiveDashboard,
|
|
setCards,
|
|
addCard,
|
|
setEditMode,
|
|
} = useDashboardStore();
|
|
|
|
const controlActive = useControlMode((s) => s.active);
|
|
const controlMode = useControlMode((s) => s.mode);
|
|
|
|
const [libOpen, setLibOpen] = useState(false);
|
|
const [settingsCardId, setSettingsCardId] = useState<string | null>(null);
|
|
const [initialized, setInitialized] = useState(false);
|
|
const canvasRef = useRef<HTMLDivElement>(null);
|
|
|
|
// 대시보드 목록 로드
|
|
const loadDashboards = useCallback(async () => {
|
|
try {
|
|
const result = await getDashboardList({ limit: 100 });
|
|
const list: Record<string, any>[] = result?.list ?? [];
|
|
setDashboards(list);
|
|
|
|
// 첫 번째 대시보드 자동 선택
|
|
if (list.length > 0 && !activeDashboardId) {
|
|
const firstId = 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]);
|
|
|
|
useEffect(() => { loadDashboards(); }, []);
|
|
|
|
// 대시보드 전환 시 카드 로드
|
|
const loadCards = useCallback(async (dashId: string) => {
|
|
try {
|
|
const cardList = await getDashboardCards(dashId);
|
|
setCards(cardList ?? []);
|
|
} catch (err) {
|
|
console.error('[Dashboard] Load cards failed:', err);
|
|
setCards([]);
|
|
}
|
|
}, [setCards]);
|
|
|
|
useEffect(() => {
|
|
if (activeDashboardId) {
|
|
loadCards(activeDashboardId);
|
|
setEditMode(false);
|
|
}
|
|
}, [activeDashboardId, loadCards, setEditMode]);
|
|
|
|
// 활성 대시보드 정보
|
|
const activeDash = dashboards.find(
|
|
(d) => (d.dashboard_id ?? d.DASHBOARD_ID) === activeDashboardId
|
|
);
|
|
const dashName = activeDash?.name ?? activeDash?.NAME ?? '대시보드';
|
|
|
|
// 대시보드 CRUD
|
|
const handleAddDashboard = async () => {
|
|
const name = prompt('새 대시보드 이름:', '새 대시보드');
|
|
if (!name?.trim()) return;
|
|
try {
|
|
const result = await insertDashboard({ name: name.trim() });
|
|
await loadDashboards();
|
|
if (result?.dashboard_id) {
|
|
setActiveDashboard(result.dashboard_id);
|
|
}
|
|
toast.success(`"${name.trim()}" 대시보드를 만들었습니다`);
|
|
} catch (err) {
|
|
toast.error('대시보드 생성 실패');
|
|
}
|
|
};
|
|
|
|
const handleRenameDashboard = async (id: string) => {
|
|
const dash = dashboards.find((d) => (d.dashboard_id ?? d.DASHBOARD_ID) === id);
|
|
if (!dash) return;
|
|
const newName = prompt('새 이름:', dash.name ?? dash.NAME ?? '');
|
|
if (!newName?.trim()) return;
|
|
try {
|
|
await updateDashboard(id, { name: newName.trim() });
|
|
await loadDashboards();
|
|
toast.success('이름 변경됨');
|
|
} catch (err) {
|
|
toast.error('이름 변경 실패');
|
|
}
|
|
};
|
|
|
|
const handleDeleteDashboard = async (id: string) => {
|
|
if (dashboards.length <= 1) {
|
|
toast.warning('마지막 대시보드는 삭제할 수 없습니다');
|
|
return;
|
|
}
|
|
const dash = dashboards.find((d) => (d.dashboard_id ?? d.DASHBOARD_ID) === id);
|
|
if (!confirm(`"${dash?.name ?? dash?.NAME}" 을 삭제합니다.`)) return;
|
|
try {
|
|
await deleteDashboard(id);
|
|
await loadDashboards();
|
|
toast.info('대시보드 삭제됨');
|
|
} catch (err) {
|
|
toast.error('삭제 실패');
|
|
}
|
|
};
|
|
|
|
const handleSwitchDashboard = (id: string) => {
|
|
if (id === activeDashboardId) return;
|
|
setActiveDashboard(id);
|
|
};
|
|
|
|
// 템플릿 추가 (라이브러리 → 카드)
|
|
const handleSelectTemplate = async (template: Record<string, any>) => {
|
|
if (!activeDashboardId) return;
|
|
const templateId = template.template_id ?? template.TEMPLATE_ID;
|
|
try {
|
|
const result = await insertDashboardCard(activeDashboardId, {
|
|
template_id: templateId,
|
|
position_x: 50 + Math.floor(Math.random() * 200),
|
|
position_y: 30 + Math.floor(Math.random() * 100),
|
|
width: 700,
|
|
height: 450,
|
|
});
|
|
// 새 카드를 store에 추가 (서버 응답 + 원본 template 정보 결합)
|
|
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: 50 + Math.floor(Math.random() * 200),
|
|
position_y: 30 + Math.floor(Math.random() * 100),
|
|
width: 700,
|
|
height: 450,
|
|
is_collapsed: false,
|
|
});
|
|
setLibOpen(false);
|
|
if (!editMode) setEditMode(true);
|
|
toast.success(`${template.name ?? template.NAME} 카드를 추가했습니다`);
|
|
} catch (err) {
|
|
toast.error('카드 추가 실패');
|
|
}
|
|
};
|
|
|
|
// 레이아웃 저장
|
|
const handleSaveLayout = 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('저장 실패');
|
|
}
|
|
};
|
|
|
|
// 설정 카드 정보
|
|
const settingsCard = settingsCardId
|
|
? cards.find((c) => (c.card_id ?? c.CARD_ID) === settingsCardId)
|
|
: null;
|
|
|
|
if (!initialized) {
|
|
return (
|
|
<div className="dash-shell" style={{ alignItems: 'center', justifyContent: 'center' }}>
|
|
<div style={{ color: 'var(--v5-text-muted)', fontSize: '.8rem' }}>로딩 중...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="dash-shell">
|
|
{/* 사이드바 — 제어 편집 모드에서는 팔레트로 교체 */}
|
|
{controlActive && controlMode === 'edit' ? (
|
|
<div className="dash-side">
|
|
<div className="dash-side-sec" style={{ color: 'var(--ctrl-cyan)' }}>제어 팔레트</div>
|
|
<ControlPalette onDropTable={() => {}} onDropControl={() => {}} />
|
|
</div>
|
|
) : (
|
|
<DashboardSidebar
|
|
onAddDashboard={handleAddDashboard}
|
|
onRenameDashboard={handleRenameDashboard}
|
|
onDeleteDashboard={handleDeleteDashboard}
|
|
onSwitchDashboard={handleSwitchDashboard}
|
|
/>
|
|
)}
|
|
<div className="dash-content">
|
|
{activeDashboardId ? (
|
|
<>
|
|
<DashboardToolbar
|
|
dashboardName={dashName}
|
|
cardCount={cards.length}
|
|
onOpenLibrary={() => setLibOpen(true)}
|
|
onSaveLayout={handleSaveLayout}
|
|
/>
|
|
{/* 제어 모드 툴바 + 오버레이 */}
|
|
<div style={{ position: 'relative', flex: 1 }}>
|
|
<DashboardCanvas
|
|
ref={canvasRef}
|
|
dashboardName={dashName}
|
|
onOpenLibrary={() => setLibOpen(true)}
|
|
onOpenSettings={(id) => setSettingsCardId(id)}
|
|
controlMode={controlActive}
|
|
/>
|
|
<ControlMode
|
|
dashboardId={activeDashboardId}
|
|
cards={cards}
|
|
canvasRef={canvasRef}
|
|
/>
|
|
{settingsCard && !controlActive && (
|
|
<CardSettingsPanel
|
|
cardId={settingsCardId!}
|
|
tableName={settingsCard.primary_table ?? settingsCard.PRIMARY_TABLE ?? ''}
|
|
onClose={() => setSettingsCardId(null)}
|
|
/>
|
|
)}
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div style={{
|
|
flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
flexDirection: 'column', gap: '.8rem',
|
|
}}>
|
|
<div style={{ fontSize: '3rem', opacity: .25 }}>📋</div>
|
|
<div style={{ fontSize: '.9rem', fontWeight: 700, color: 'var(--v5-text-sec)' }}>
|
|
대시보드가 없습니다
|
|
</div>
|
|
<button className="dash-empty-btn" onClick={handleAddDashboard}>
|
|
+ 새 대시보드 만들기
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<TemplateLibraryModal
|
|
open={libOpen}
|
|
onClose={() => setLibOpen(false)}
|
|
onSelectTemplate={handleSelectTemplate}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|