import { useState, useEffect, useRef } from 'react'; import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query'; import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, DragOverlay, type DragStartEvent, type DragEndEvent, } from '@dnd-kit/core'; import { arrayMove } from '@dnd-kit/sortable'; import { apiClient } from '../lib/apiClient'; import { useTasks } from '../hooks/useTasks'; import { useTeamMembers } from '../hooks/useTeamMembers'; import { DashboardHeader } from '../components/dashboard/DashboardHeader'; import { TeamStatusPanel } from '../components/dashboard/TeamStatusPanel'; import { DepartmentColumn } from '../components/dashboard/DepartmentColumn'; import { TaskManager } from '../components/dashboard/TaskManager'; import { useSocket } from '../contexts/SocketContext'; import { sendTaskSelected, openDetailWindow } from '../lib/dualMonitor'; import { isRoutineTask, displayFlagsForTaskType } from '../lib/taskType'; import { DEFAULT_STATUS_FILTERS, taskMatchesStatusFilters, toggleAllFilter, toggleCoreFilter, type CoreStatusFilter, } from '../lib/statusFilters'; const QUARTER = '2026-Q2'; const SECTIONS = ['인사관리', '학습성장', '운영지원', '전산관리'] as const; const COLUMN_STYLES = [ { section: '인사관리', titleEn: 'HR Management', headerStyle: { background: 'linear-gradient(120deg,#2a4a8a 0%,#3461b8 50%,#3d72d0 100%)' } }, { section: '학습성장', titleEn: 'Learning & Growth', headerStyle: { background: 'linear-gradient(120deg,#5b2d8a 0%,#7340b8 50%,#8a52d0 100%)' } }, { section: '운영지원', titleEn: 'Operations', headerStyle: { background: 'linear-gradient(120deg,#0d6080 0%,#0d7a9a 50%,#0e92b8 100%)' } }, { section: '전산관리', titleEn: 'IT Management', headerStyle: { background: 'linear-gradient(120deg,#0a6040 0%,#0d8050 50%,#10a060 100%)' } }, ] as const; export default function DashboardPage() { const [activeFilters, setActiveFilters] = useState([...DEFAULT_STATUS_FILTERS]); const [filtersBeforeIssue, setFiltersBeforeIssue] = useState([...DEFAULT_STATUS_FILTERS]); const [issueFilterActive, setIssueFilterActive] = useState(false); const [showTaskManager, setShowTaskManager] = useState(false); const [teamPanelOpen, setTeamPanelOpen] = useState(false); const [showAllTeamTasks, setShowAllTeamTasks] = useState(false); const [activeTeamProjectId, setActiveTeamProjectId] = useState(null); const [activeTaskId, setActiveTaskId] = useState(null); const [columnOrders, setColumnOrders] = useState>({}); const saveTimers = useRef>>({}); const queryClient = useQueryClient(); const socket = useSocket(); const { data: tasks = [], isLoading } = useTasks({ quarter: QUARTER }); const { data: teamMembers = [] } = useTeamMembers(); 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, }); // colConfig의 cardOrder로 초기 순서 설정 useEffect(() => { if (!colConfigs) return; setColumnOrders((prev) => { const next = { ...prev }; colConfigs.forEach((c) => { if (c.cardOrder && !next[c.key]) { try { next[c.key] = JSON.parse(c.cardOrder); } catch { /* ignore */ } } }); return next; }); }, [colConfigs]); // 새 태스크 추가 시 순서 목록에 병합 useEffect(() => { SECTIONS.forEach((section) => { const ids = tasks.filter((t) => t.section === section).map((t) => t.id); setColumnOrders((prev) => { const existing = prev[section] ?? []; const merged = [ ...existing.filter((id) => ids.includes(id)), ...ids.filter((id) => !existing.includes(id)), ]; if (JSON.stringify(merged) === JSON.stringify(existing)) return prev; return { ...prev, [section]: merged }; }); }); }, [tasks]); const patchColumn = useMutation({ mutationFn: ({ section, data }: { section: string; data: Record }) => apiClient.patch(`/columns/${encodeURIComponent(section)}`, data), }); const patchTask = useMutation({ mutationFn: ({ id, data }: { id: string; data: Record }) => apiClient.patch(`/tasks/${id}`, data), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['tasks'] }), }); 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 sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } })); const handleDragStart = (event: DragStartEvent) => { setActiveTaskId(String(event.active.id)); }; const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; setActiveTaskId(null); if (!over) return; const activeId = String(active.id); const overId = String(over.id); const draggedTask = tasks.find((t) => t.id === activeId); if (!draggedTask) return; const srcSection = draggedTask.section ?? ''; // ── 드롭 대상이 컬럼 드롭존인 경우 ────────────────────────── if (overId.startsWith('drop::')) { // 형식: "drop::project::섹션명" 또는 "drop::routine::섹션명" const parts = overId.split('::'); const areaType = parts[1]; // 'project' | 'routine' const targetSection = parts[2]; const updateData: Record = {}; if (targetSection !== srcSection) updateData.section = targetSection; if (areaType === 'routine' && !isRoutineTask(draggedTask.taskType)) { updateData.taskType = '기반업무'; Object.assign(updateData, displayFlagsForTaskType('기반업무')); } else if (areaType === 'project' && isRoutineTask(draggedTask.taskType)) { updateData.taskType = '실행과제'; Object.assign(updateData, displayFlagsForTaskType('실행과제')); } if (Object.keys(updateData).length > 0) { patchTask.mutate({ id: activeId, data: updateData }); } return; } // ── 드롭 대상이 다른 카드인 경우 ───────────────────────────── const overTask = tasks.find((t) => t.id === overId); if (!overTask) return; const dstSection = overTask.section ?? ''; const typeChanged = isRoutineTask(draggedTask.taskType) !== isRoutineTask(overTask.taskType); if (dstSection !== srcSection || typeChanged) { const updateData: Record = {}; if (dstSection !== srcSection) updateData.section = dstSection; if (typeChanged) { const newType = isRoutineTask(overTask.taskType) ? '기반업무' : '실행과제'; updateData.taskType = newType; Object.assign(updateData, displayFlagsForTaskType(newType)); } patchTask.mutate({ id: activeId, data: updateData }); if (dstSection !== srcSection) return; } // ── 같은 컬럼 내 순서 변경 ─────────────────────────────────── const current = columnOrders[srcSection] ?? tasks.filter((t) => t.section === srcSection).map((t) => t.id); const oldIdx = current.indexOf(activeId); const newIdx = current.indexOf(overId); if (oldIdx === -1 || newIdx === -1 || oldIdx === newIdx) return; const newOrder = arrayMove(current, oldIdx, newIdx); setColumnOrders((prev) => ({ ...prev, [srcSection]: newOrder })); if (saveTimers.current[srcSection]) clearTimeout(saveTimers.current[srcSection]); saveTimers.current[srcSection] = setTimeout(() => { patchColumn.mutate({ section: srcSection, data: { cardOrder: JSON.stringify(newOrder) } }); }, 300); }; const stats = { total: tasks.length, inProgress: tasks.filter((t) => t.status === 'IN_PROGRESS').length, review: tasks.filter((t) => t.status === 'REVIEW' || t.status === 'CANCELLED' || t.status === 'TODO').length, done: tasks.filter((t) => t.status === 'DONE').length, issues: tasks.filter((t) => !!t.issueNote).length, }; const filtered = tasks.filter((t) => { if (issueFilterActive) return !!t.issueNote; return taskMatchesStatusFilters(t, activeFilters); }); const handleToggleAll = () => { setIssueFilterActive(false); setActiveFilters((prev) => toggleAllFilter(prev)); }; const handleToggleStatus = (key: CoreStatusFilter) => { setIssueFilterActive(false); setActiveFilters((prev) => toggleCoreFilter(prev, key)); }; const handleToggleIssue = () => { if (issueFilterActive) { setIssueFilterActive(false); setActiveFilters([...filtersBeforeIssue]); return; } setFiltersBeforeIssue([...activeFilters]); setIssueFilterActive(true); }; const sectionOptions = SECTIONS.map((s) => ({ value: s, label: colConfigs?.find((c) => c.key === s)?.title ?? s, })); const activeTask = tasks.find((t) => t.id === activeTaskId) ?? null; if (isLoading) { return (
데이터를 불러오는 중...
); } return (
{ openDetailWindow(); }} onOpenTaskManager={() => setShowTaskManager(true)} teamPanelOpen={teamPanelOpen} onToggleTeamPanel={() => { setTeamPanelOpen((open) => { if (open) { setActiveTeamProjectId(null); setShowAllTeamTasks(false); } return !open; }); }} /> {teamPanelOpen && ( setShowAllTeamTasks((v) => !v)} onProjectClick={setActiveTeamProjectId} onClose={() => { setTeamPanelOpen(false); setActiveTeamProjectId(null); setShowAllTeamTasks(false); }} /> )}
{/* ── 좌측 라벨 컬럼 ── */}
실행과제
기반업무
{COLUMN_STYLES.map(({ section, titleEn, headerStyle }) => ( t.section === section)} orderedIds={columnOrders[section] ?? []} headerBg="" headerStyle={headerStyle} storageKey={`col_${section}`} section={section} quarter={QUARTER} onSelectTask={(t) => sendTaskSelected(t.id)} sectionOptions={sectionOptions} teamMembers={teamMembers} /> ))}
{activeTask ? (
{activeTask.title} = 70 ? 'text-emerald-500' : activeTask.progress >= 40 ? 'text-blue-400' : 'text-orange-400' }`}>{activeTask.progress}%
) : null}
{showTaskManager && ( setShowTaskManager(false)} /> )}
); }