feat: team org panel, admin CRUD, local deploy tools, bidirectional data sync

Add TeamMember model and APIs, team status UI, /admin page, local server bats,
and scripts to sync data between local PostgreSQL and Render.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
EENE Dashboard
2026-06-06 01:41:00 +09:00
parent d14ff1997c
commit fb2956b0ac
45 changed files with 4104 additions and 376 deletions

View File

@@ -13,7 +13,9 @@ import {
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';
@@ -42,6 +44,9 @@ export default function DashboardPage() {
const [filtersBeforeIssue, setFiltersBeforeIssue] = useState<string[]>([...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<string | null>(null);
const [activeTaskId, setActiveTaskId] = useState<string | null>(null);
const [columnOrders, setColumnOrders] = useState<Record<string, string[]>>({});
const saveTimers = useRef<Record<string, ReturnType<typeof setTimeout>>>({});
@@ -50,6 +55,7 @@ export default function DashboardPage() {
const socket = useSocket();
const { data: tasks = [], isLoading } = useTasks({ quarter: QUARTER });
const { data: teamMembers = [] } = useTeamMembers();
const { data: colConfigs } = useQuery({
queryKey: ['columns', 'all'],
@@ -248,9 +254,35 @@ export default function DashboardPage() {
onToggleIssue={handleToggleIssue}
onOpenDetailWindow={() => { openDetailWindow(); }}
onOpenTaskManager={() => setShowTaskManager(true)}
teamPanelOpen={teamPanelOpen}
onToggleTeamPanel={() => {
setTeamPanelOpen((open) => {
if (open) {
setActiveTeamProjectId(null);
setShowAllTeamTasks(false);
}
return !open;
});
}}
/>
<main className="relative flex min-h-0 flex-1 overflow-hidden px-5 py-5">
{teamPanelOpen && (
<TeamStatusPanel
members={teamMembers}
tasks={tasks}
showAllTasks={showAllTeamTasks}
activeProjectId={activeTeamProjectId}
onToggleShowAll={() => setShowAllTeamTasks((v) => !v)}
onProjectClick={setActiveTeamProjectId}
onClose={() => {
setTeamPanelOpen(false);
setActiveTeamProjectId(null);
setShowAllTeamTasks(false);
}}
/>
)}
<main className={`relative flex min-h-0 flex-1 overflow-hidden px-5 py-5 ${teamPanelOpen ? 'hidden' : ''}`}>
{/* ── 좌측 라벨 컬럼 ── */}
<div className="mr-4 flex w-16 shrink-0 flex-col overflow-hidden rounded-[2rem] bg-white shadow-[0_16px_40px_rgba(15,23,42,0.12)] ring-1 ring-slate-200/70">
<div className="h-10 shrink-0 border-b border-slate-100 bg-slate-50" />
@@ -283,6 +315,7 @@ export default function DashboardPage() {
quarter={QUARTER}
onSelectTask={(t) => sendTaskSelected(t.id)}
sectionOptions={sectionOptions}
teamMembers={teamMembers}
/>
))}
</div>
@@ -307,6 +340,7 @@ export default function DashboardPage() {
tasks={tasks}
sectionOptions={sectionOptions}
quarter={QUARTER}
teamMembers={teamMembers}
onClose={() => setShowTaskManager(false)}
/>
)}