diff --git a/frontend/src/components/dashboard/DashboardHeader.tsx b/frontend/src/components/dashboard/DashboardHeader.tsx index 4ed56b0..5e0e87e 100644 --- a/frontend/src/components/dashboard/DashboardHeader.tsx +++ b/frontend/src/components/dashboard/DashboardHeader.tsx @@ -10,14 +10,13 @@ interface Stats { interface DashboardHeaderProps { quarter: string; stats: Stats; - activeType: string; - onTypeChange: (type: string) => void; activeStatus: string; onStatusChange: (status: string) => void; onOpenDetailWindow: () => void; + onOpenTaskManager: () => void; } -export function DashboardHeader({ quarter, stats, activeType, onTypeChange, activeStatus, onStatusChange, onOpenDetailWindow }: DashboardHeaderProps) { +export function DashboardHeader({ quarter, stats, activeStatus, onStatusChange, onOpenDetailWindow, onOpenTaskManager }: DashboardHeaderProps) { const today = new Date(); const todayStr = `${today.getMonth() + 1}월 ${today.getDate()}일`; @@ -51,7 +50,7 @@ export function DashboardHeader({ quarter, stats, activeType, onTypeChange, acti )} - {/* ── 오른쪽: 날짜 + 업무유형 필터 ── */} + {/* ── 오른쪽: 버튼들 + 날짜 ── */}
+
{todayStr} -
-
- {['전체', '상시업무', '프로젝트'].map((type) => ( - - ))} -
diff --git a/frontend/src/components/dashboard/DepartmentColumn.tsx b/frontend/src/components/dashboard/DepartmentColumn.tsx index 14c354a..ebac9ff 100644 --- a/frontend/src/components/dashboard/DepartmentColumn.tsx +++ b/frontend/src/components/dashboard/DepartmentColumn.tsx @@ -257,7 +257,7 @@ export function DepartmentColumn({ title: initialTitle, titleEn, subtitle: initi const routineTasks = orderedTasks.filter((t) => t.taskType === '상시업무'); return (
-
+
{routineTasks.length === 0 ? (
관리현황 없음 diff --git a/frontend/src/components/dashboard/TaskManager.tsx b/frontend/src/components/dashboard/TaskManager.tsx new file mode 100644 index 0000000..6b79fea --- /dev/null +++ b/frontend/src/components/dashboard/TaskManager.tsx @@ -0,0 +1,226 @@ +import { useState } from 'react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { createPortal } from 'react-dom'; +import { apiClient } from '../../lib/apiClient'; +import { TaskModal } from '../common/TaskModal'; +import type { TaskFormData } from '../common/TaskModal'; +import type { Task } from '../../types'; + +const STATUS_LABEL: Record = { + IN_PROGRESS: '진행', REVIEW: '보류', TODO: '대기', DONE: '완료', CANCELLED: '취소', +}; +const STATUS_STYLE: Record = { + IN_PROGRESS: 'bg-blue-100 text-blue-700', + REVIEW: 'bg-orange-100 text-orange-700', + TODO: 'bg-gray-100 text-gray-600', + DONE: 'bg-emerald-100 text-emerald-700', + CANCELLED: 'bg-gray-100 text-gray-400', +}; + +const SECTIONS = ['인사관리', '학습성장', '운영지원', '전산관리'] as const; + +interface TaskManagerProps { + tasks: Task[]; + sectionOptions: { value: string; label: string }[]; + quarter: string; + onClose: () => void; +} + +export function TaskManager({ tasks, sectionOptions, quarter, onClose }: TaskManagerProps) { + const queryClient = useQueryClient(); + const [filterSection, setFilterSection] = useState('전체'); + const [filterType, setFilterType] = useState('전체'); + const [modalMode, setModalMode] = useState<'add' | 'edit' | null>(null); + const [editingTask, setEditingTask] = useState(null); + + const create = useMutation({ + mutationFn: (data: Record) => apiClient.post('/tasks', data), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['tasks'] }), + }); + const patch = useMutation({ + mutationFn: ({ id, data }: { id: string; data: Record }) => + apiClient.patch(`/tasks/${id}`, data), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['tasks'] }), + }); + const remove = useMutation({ + mutationFn: (id: string) => apiClient.delete(`/tasks/${id}`), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['tasks'] }), + }); + + const filtered = tasks.filter((t) => { + if (filterSection !== '전체' && t.section !== filterSection) return false; + if (filterType !== '전체' && t.taskType !== filterType) return false; + return true; + }); + + const handleAdd = (data: TaskFormData) => { + create.mutate({ + title: data.title, section: data.section || null, tag: data.tag || null, + taskType: data.taskType || null, status: data.status, progress: data.progress, + description: data.description || null, issueNote: data.issueNote || null, + startDate: data.startDate || null, dueDate: data.dueDate || null, + quarter: data.quarter, priority: 'MEDIUM', creatorId: 'system', + }); + setModalMode(null); + }; + + const handleEdit = (data: TaskFormData) => { + if (!editingTask) return; + patch.mutate({ + id: editingTask.id, + data: { + title: data.title, section: data.section || null, tag: data.tag || null, + taskType: data.taskType || null, status: data.status, progress: data.progress, + description: data.description || null, issueNote: data.issueNote || null, + startDate: data.startDate || null, dueDate: data.dueDate || null, + }, + }); + setModalMode(null); + setEditingTask(null); + }; + + const handleDelete = (task: Task) => { + if (window.confirm(`"${task.title}" 업무를 삭제하시겠습니까?`)) { + remove.mutate(task.id); + } + }; + + return createPortal( +
+
e.stopPropagation()} + > + {/* 헤더 */} +
+

📋 업무관리

+
+ {/* 부문 필터 */} +
+ {['전체', ...SECTIONS].map((s) => ( + + ))} +
+
+ {/* 유형 필터 */} +
+ {['전체', '프로젝트', '상시업무'].map((t) => ( + + ))} +
+
+ + +
+
+ + {/* 테이블 */} +
+ + + + + + + + + + + + + + + {filtered.length === 0 ? ( + + ) : filtered.map((task) => ( + + + + + + + + + + + + ))} + +
부문유형업무명내용기간진행률상태이슈 +
업무 없음
{task.section ?? '-'} + + {task.taskType ?? '프로젝트'} + + {task.title}{task.description ?? '-'} + {task.startDate || task.dueDate + ? `${task.startDate?.slice(0,10) ?? '?'} ~ ${task.dueDate?.slice(0,10) ?? '?'}` + : '-'} + + = 70 ? 'text-emerald-500' : task.progress >= 40 ? 'text-blue-500' : 'text-orange-400' + }`}>{task.progress}% + + + {STATUS_LABEL[task.status]} + + {task.issueNote ?? ''} +
+ + +
+
+
+ + {/* 하단 요약 */} +
+ 전체 {filtered.length} + 프로젝트 {filtered.filter(t => t.taskType !== '상시업무').length} + 관리현황 {filtered.filter(t => t.taskType === '상시업무').length} +
+
+ + {modalMode === 'add' && ( + setModalMode(null)} + /> + )} + {modalMode === 'edit' && editingTask && ( + { setModalMode(null); setEditingTask(null); }} + /> + )} +
, + document.body + ); +} diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index 51b9117..7e67011 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -4,14 +4,15 @@ import { apiClient } from '../lib/apiClient'; import { useTasks } from '../hooks/useTasks'; import { DashboardHeader } from '../components/dashboard/DashboardHeader'; import { DepartmentColumn } from '../components/dashboard/DepartmentColumn'; +import { TaskManager } from '../components/dashboard/TaskManager'; 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 [showTaskManager, setShowTaskManager] = useState(false); const queryClient = useQueryClient(); const socket = useSocket(); @@ -25,18 +26,16 @@ export default function DashboardPage() { 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, + total: tasks.length, + inProgress: tasks.filter((t) => t.status === 'IN_PROGRESS').length, + review: tasks.filter((t) => t.status === 'REVIEW' || t.status === 'CANCELLED').length, + waiting: tasks.filter((t) => t.status === 'TODO').length, + done: tasks.filter((t) => t.status === 'DONE').length, + issues: tasks.filter((t) => !!t.issueNote).length, }; - const filtered = byType.filter((t) => { + const filtered = tasks.filter((t) => { if (activeStatus === '전체') return true; if (activeStatus === 'ISSUES') return !!t.issueNote; if (activeStatus === 'REVIEW') return t.status === 'REVIEW' || t.status === 'CANCELLED'; @@ -78,11 +77,10 @@ export default function DashboardPage() { { setActiveType(type); setActiveStatus('전체'); }} activeStatus={activeStatus} onStatusChange={setActiveStatus} onOpenDetailWindow={openDetailWindow} + onOpenTaskManager={() => setShowTaskManager(true)} />
@@ -138,6 +136,15 @@ export default function DashboardPage() {
+ + {showTaskManager && ( + setShowTaskManager(false)} + /> + )}
); }