Initial commit - EENE Dashboard

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
EENE Dashboard
2026-05-29 18:07:10 +09:00
commit 22366dde72
64 changed files with 10483 additions and 0 deletions

View File

@@ -0,0 +1,168 @@
import { useState, useEffect } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '../lib/apiClient';
import { useTasks } from '../hooks/useTasks';
import { DashboardHeader } from '../components/dashboard/DashboardHeader';
import { DepartmentColumn } from '../components/dashboard/DepartmentColumn';
import { RoutinePanel } from '../components/dashboard/RoutinePanel';
import { useSocket } from '../contexts/SocketContext';
import { sendTaskSelected, openDetailWindow } from '../lib/dualMonitor';
const QUARTER = '2026-Q2';
export default function DashboardPage() {
const [activeType, setActiveType] = useState('전체');
const [activeStatus, setActiveStatus] = useState('전체');
const [isBottomPanelOpen, setIsBottomPanelOpen] = useState(false);
const queryClient = useQueryClient();
const socket = useSocket();
const { data: tasks = [], isLoading } = useTasks({ quarter: QUARTER });
useEffect(() => {
if (!socket) return;
const refresh = () => queryClient.invalidateQueries({ queryKey: ['tasks'] });
socket.on('tasks:refresh', refresh);
socket.on('task:updated', refresh);
return () => { socket.off('tasks:refresh', refresh); socket.off('task:updated', refresh); };
}, [socket, queryClient]);
const byType = tasks.filter((t) => activeType === '전체' || t.taskType === activeType);
const stats = {
total: byType.length,
inProgress: byType.filter((t) => t.status === 'IN_PROGRESS').length,
review: byType.filter((t) => t.status === 'REVIEW' || t.status === 'CANCELLED').length,
waiting: byType.filter((t) => t.status === 'TODO').length,
done: byType.filter((t) => t.status === 'DONE').length,
issues: byType.filter((t) => !!t.issueNote).length,
};
const filtered = byType.filter((t) => {
if (activeStatus === '전체') return true;
if (activeStatus === 'ISSUES') return !!t.issueNote;
if (activeStatus === 'REVIEW') return t.status === 'REVIEW' || t.status === 'CANCELLED';
return t.status === activeStatus;
});
const SECTIONS = ['인사관리', '학습성장', '운영지원', '전산관리'] as const;
const sec1Tasks = filtered.filter((t) => t.section === '인사관리');
const sec2Tasks = filtered.filter((t) => t.section === '학습성장');
const sec3Tasks = filtered.filter((t) => t.section === '운영지원');
const sec4Tasks = filtered.filter((t) => t.section === '전산관리');
const routineTasks = tasks.filter((t) => t.taskType === '상시업무');
const { data: colConfigs } = useQuery({
queryKey: ['columns', 'all'],
queryFn: async () => {
const results = await Promise.all(
SECTIONS.map((s) => apiClient.get(`/columns/${encodeURIComponent(s)}`).then((r) => ({ key: s, ...r.data }))),
);
return results;
},
staleTime: 0,
});
const sectionOptions = SECTIONS.map((s) => ({
value: s,
label: colConfigs?.find((c) => c.key === s)?.title ?? s,
}));
if (isLoading) {
return (
<div className="flex h-screen items-center justify-center bg-slate-100">
<div className="text-3xl text-gray-400"> ...</div>
</div>
);
}
return (
<div className="relative flex flex-col h-screen bg-slate-100 overflow-hidden" style={{ fontSize: '18px' }}>
<DashboardHeader
quarter={QUARTER}
stats={stats}
activeType={activeType}
onTypeChange={(type) => { setActiveType(type); setActiveStatus('전체'); }}
activeStatus={activeStatus}
onStatusChange={setActiveStatus}
onOpenDetailWindow={openDetailWindow}
/>
<main className="relative flex-1 overflow-hidden min-h-0">
<div className="grid h-full grid-cols-4 overflow-hidden min-h-0">
<DepartmentColumn
title="인사관리"
titleEn="HR Management"
tasks={sec1Tasks}
headerBg=""
headerStyle={{ background: 'linear-gradient(120deg, #2a4a8a 0%, #3461b8 50%, #3d72d0 100%)' }}
storageKey="col_sec1"
section="인사관리"
quarter={QUARTER}
onSelectTask={(t) => sendTaskSelected(t.id)}
sectionOptions={sectionOptions}
/>
<DepartmentColumn
title="학습성장"
titleEn="Learning & Growth"
tasks={sec2Tasks}
headerBg=""
headerStyle={{ background: 'linear-gradient(120deg, #5b2d8a 0%, #7340b8 50%, #8a52d0 100%)' }}
storageKey="col_sec2"
section="학습성장"
quarter={QUARTER}
onSelectTask={(t) => sendTaskSelected(t.id)}
sectionOptions={sectionOptions}
/>
<DepartmentColumn
title="운영지원"
titleEn="Operations"
tasks={sec3Tasks}
headerBg=""
headerStyle={{ background: 'linear-gradient(120deg, #0d6080 0%, #0d7a9a 50%, #0e92b8 100%)' }}
storageKey="col_sec3"
section="운영지원"
quarter={QUARTER}
onSelectTask={(t) => sendTaskSelected(t.id)}
sectionOptions={sectionOptions}
/>
<DepartmentColumn
title="전산관리"
titleEn="IT Management"
tasks={sec4Tasks}
headerBg=""
headerStyle={{ background: 'linear-gradient(120deg, #0a6040 0%, #0d8050 50%, #10a060 100%)' }}
storageKey="col_sec4"
section="전산관리"
quarter={QUARTER}
onSelectTask={(t) => sendTaskSelected(t.id)}
sectionOptions={sectionOptions}
/>
</div>
{/* 하단 슬라이드 패널 */}
<button
type="button"
onClick={() => setIsBottomPanelOpen((v) => !v)}
className={`absolute left-1/2 z-40 -translate-x-1/2 rounded-t-2xl border border-orange-200 bg-orange-50 px-10 py-1.5 text-orange-600 shadow-md transition-all hover:bg-orange-100 ${
isBottomPanelOpen ? 'top-0' : 'bottom-0'
}`}
aria-label={isBottomPanelOpen ? '하단 정보 닫기' : '하단 정보 열기'}
>
<span className={`block text-2xl font-black leading-none transition-transform ${isBottomPanelOpen ? 'rotate-180' : ''}`}>
^
</span>
</button>
<section
className={`absolute inset-x-0 bottom-0 z-30 h-full rounded-t-3xl border-t border-gray-200 bg-white shadow-2xl transition-transform duration-300 ease-out overflow-hidden ${
isBottomPanelOpen ? 'translate-y-0' : 'translate-y-full'
}`}
>
<RoutinePanel tasks={routineTasks} />
</section>
</main>
</div>
);
}