Initial commit - EENE Dashboard
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
168
frontend/src/pages/DashboardPage.tsx
Normal file
168
frontend/src/pages/DashboardPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user