diff --git a/src/app/(main)/project/workspace/[id]/page.tsx b/src/app/(main)/project/workspace/[id]/page.tsx index 9992c24..fe56b47 100644 --- a/src/app/(main)/project/workspace/[id]/page.tsx +++ b/src/app/(main)/project/workspace/[id]/page.tsx @@ -7,6 +7,7 @@ import dynamic from "next/dynamic"; import { queryKeys } from "@/lib/constants/queryKeys"; import BottomNavigation from "@/components/layout/BottomNavigation"; +import { useWorkspaceNav } from "@/providers/WorkspaceNavProvider"; import { Task } from "@/types/kanban"; import { showToast } from "@/lib/utils/toast"; @@ -37,10 +38,10 @@ export default function ProjectPage() { const queryClient = useQueryClient(); const [kanbanBoardId, setKanbanBoardId] = useState(""); - const [currentView, setCurrentView] = useState("kanban"); - const [showMemoPanel, setShowMemoPanel] = useState(false); const [showProjectInfoPanel, setShowProjectInfoPanel] = useState(false); + const { currentView, showMemoPanel, setView, toggleMemo, closeMemo, setProjectContext, clearWorkspace } = useWorkspaceNav(); + const userId = session?.user?.user_id; const taskQueryKey = queryKeys.tasks.list(projectId); @@ -288,14 +289,23 @@ export default function ProjectPage() { queryClient.invalidateQueries({ queryKey: taskQueryKey }); }; + useEffect(() => { + if (projectName) setProjectContext(projectName); + }, [projectName, setProjectContext]); + + useEffect(() => { + return () => clearWorkspace(); + }, [clearWorkspace]); + const handleViewChange = (view: NavItem) => { if (view === "memo") { - setShowMemoPanel((prev) => !prev); + toggleMemo(); setShowProjectInfoPanel(false); } else if (view === "project") { router.push("/"); } else { - setCurrentView(view); + setView(view as "kanban" | "calendar"); + setShowProjectInfoPanel(false); } }; @@ -318,7 +328,7 @@ export default function ProjectPage() { return (
-
+
{/* 메인 콘텐츠 */}
@@ -329,9 +339,9 @@ export default function ProjectPage() { onCreateTask={handleCreateTask} onUpdateTask={handleUpdateTask} onDeleteTask={handleDeleteTask} - onToggleMemo={()=>{setShowMemoPanel((prev)=>!prev)}} + onToggleMemo={toggleMemo} showMemoPanel={showMemoPanel} - onProjectInfoClick={() => { setShowProjectInfoPanel((prev) => !prev); setShowMemoPanel(false); }} + onProjectInfoClick={() => { setShowProjectInfoPanel((prev) => !prev); closeMemo(); }} project={{ project_id: projectId, project_name: projectName, @@ -356,12 +366,11 @@ export default function ProjectPage() { onDeleteTask={handleDeleteTask} onSelectTask={() => {}} onTaskCreated={handleRefresh} - onToggleMemo={()=>{setShowMemoPanel((prev)=>!prev)}} + onToggleMemo={toggleMemo} showMemoPanel={showMemoPanel} - onProjectInfoClick={() => { setShowProjectInfoPanel((prev) => !prev); setShowMemoPanel(false); }} + onProjectInfoClick={() => { setShowProjectInfoPanel((prev) => !prev); closeMemo(); }} /> )} -
{/* 프로젝트 정보 side panel */} @@ -390,11 +399,11 @@ export default function ProjectPage() { : "w-0 opacity-0" }`} > - setShowMemoPanel(false)} /> +
-
+
-
-
-
{children}
- -
+ +
+
+
+ +
{children}
+
+ +
+
diff --git a/src/components/features/kanban/KanbanBoard.tsx b/src/components/features/kanban/KanbanBoard.tsx index faef548..f1b4605 100644 --- a/src/components/features/kanban/KanbanBoard.tsx +++ b/src/components/features/kanban/KanbanBoard.tsx @@ -60,6 +60,7 @@ const KanbanBoard = ({ onSelectTask, onTaskCreated, onProjectInfoClick, + onPanelOpen, showMemoPanel, onToggleMemo, }: KanbanBoardProps) => { diff --git a/src/components/layout/AppSidebar.tsx b/src/components/layout/AppSidebar.tsx new file mode 100644 index 0000000..7754cbf --- /dev/null +++ b/src/components/layout/AppSidebar.tsx @@ -0,0 +1,143 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { useSession } from "next-auth/react"; +import { useWorkspaceNav } from "@/providers/WorkspaceNavProvider"; + +const globalNavItems = [ + { + href: "/", + label: "프로젝트", + icon: ( + + + + ), + }, + { + href: "/notice", + label: "공지사항", + icon: ( + + + + ), + }, +]; + +const workspaceNavItems = [ + { + id: "kanban" as const, + label: "칸반보드", + icon: ( + + + + ), + }, + { + id: "calendar" as const, + label: "캘린더", + icon: ( + + + + ), + }, + { + id: "memo" as const, + label: "메모", + icon: ( + + + + ), + }, +]; + +export default function AppSidebar() { + const pathname = usePathname(); + const { data: session } = useSession(); + const { isActive, projectName, currentView, showMemoPanel, setView, toggleMemo } = useWorkspaceNav(); + + const isGlobalActive = (href: string) => { + if (href === "/") return pathname === "/"; + return pathname.startsWith(href); + }; + + const isWorkspaceItemActive = (id: string) => { + if (id === "memo") return showMemoPanel; + return currentView === id && !showMemoPanel; + }; + + return ( + + ); +} diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index c0337df..636cd67 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -30,13 +30,25 @@ export function Header() { return ( <>
-
- - Taskry - 프로젝트 관리 플랫폼 - +
+ {/* 로고 존 — 사이드바 너비와 일치 (데스크탑) */} +
+ + Taskry + 프로젝트 관리 플랫폼 + +
+ + {/* 로고 (모바일) */} +
+ + Taskry + 프로젝트 관리 플랫폼 + +
-
+ {/* 우측 컨트롤 */} +
+ ))} +
+
+ + {/* 담당자 */} +
+ 담당자 +
+ {(["all", "me", "assigned", "unassigned"] as const).map((v) => ( + + ))} +
+
+ + {/* 기간 */} +
+ 기간 +
+ {(["all", "today", "thisWeek", "overdue"] as const).map((v) => ( + + ))} +
+
+ + {/* 결과 카운트 + 초기화 */} +
+ + {taskCount}개 / 전체 {totalCount}개 + {taskCount === 0 && hasActiveFilter && ( + — 조건에 맞는 작업이 없어요 + )} + + {hasActiveFilter && onReset && ( + + )} +
+
+
+ ); +} diff --git a/src/providers/WorkspaceNavProvider.tsx b/src/providers/WorkspaceNavProvider.tsx new file mode 100644 index 0000000..68ee7c1 --- /dev/null +++ b/src/providers/WorkspaceNavProvider.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { createContext, useContext, useState, useCallback, ReactNode } from "react"; + +type WorkspaceView = "kanban" | "calendar"; + +interface WorkspaceNavContextType { + isActive: boolean; + projectName: string; + currentView: WorkspaceView; + showMemoPanel: boolean; + setView: (view: WorkspaceView) => void; + toggleMemo: () => void; + closeMemo: () => void; + setProjectContext: (name: string) => void; + clearWorkspace: () => void; +} + +const WorkspaceNavContext = createContext(null); + +export function WorkspaceNavProvider({ children }: { children: ReactNode }) { + const [isActive, setIsActive] = useState(false); + const [projectName, setProjectName] = useState(""); + const [currentView, setCurrentView] = useState("kanban"); + const [showMemoPanel, setShowMemoPanel] = useState(false); + + const setView = useCallback((view: WorkspaceView) => { + setCurrentView(view); + setShowMemoPanel(false); + }, []); + + const toggleMemo = useCallback(() => { + setShowMemoPanel((prev) => !prev); + }, []); + + const closeMemo = useCallback(() => { + setShowMemoPanel(false); + }, []); + + const setProjectContext = useCallback((name: string) => { + setIsActive(true); + setProjectName(name); + }, []); + + const clearWorkspace = useCallback(() => { + setIsActive(false); + setProjectName(""); + setCurrentView("kanban"); + setShowMemoPanel(false); + }, []); + + return ( + + {children} + + ); +} + +export function useWorkspaceNav() { + const ctx = useContext(WorkspaceNavContext); + if (!ctx) throw new Error("useWorkspaceNav must be used within WorkspaceNavProvider"); + return ctx; +}