From 9bdaf52a8d3193988f3ca6c766ab01bc0d0fd781 Mon Sep 17 00:00:00 2001 From: nelee Date: Thu, 14 May 2026 13:33:56 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(layout):=20=EC=9B=8C=ED=81=AC=EC=8A=A4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=82=AC=EC=9D=B4=EB=93=9C=20?= =?UTF-8?q?=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 데스크탑에서 BottomNavigation을 WorkspaceSidebar(240px)로 대체. 칸반/캘린더/메모 전환 및 프로젝트 목록 링크 포함. 모바일(< md)에서는 기존 BottomNavigation 유지. Co-Authored-By: Claude Sonnet 4.6 --- .../(main)/project/workspace/[id]/page.tsx | 13 ++- src/components/layout/WorkspaceSidebar.tsx | 98 +++++++++++++++++++ 2 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 src/components/layout/WorkspaceSidebar.tsx diff --git a/src/app/(main)/project/workspace/[id]/page.tsx b/src/app/(main)/project/workspace/[id]/page.tsx index 9992c24..008aa2d 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 WorkspaceSidebar from "@/components/layout/WorkspaceSidebar"; import { Task } from "@/types/kanban"; import { showToast } from "@/lib/utils/toast"; @@ -318,7 +319,14 @@ export default function ProjectPage() { return (
-
+
+ +
{/* 메인 콘텐츠 */}
@@ -392,9 +400,10 @@ export default function ProjectPage() { > setShowMemoPanel(false)} /> +
-
+
void; +} + +const navItems = [ + { + id: "kanban" as NavItem, + label: "칸반보드", + icon: ( + + + + ), + }, + { + id: "calendar" as NavItem, + label: "캘린더", + icon: ( + + + + ), + }, + { + id: "memo" as NavItem, + label: "메모", + icon: ( + + + + ), + }, +]; + +export default function WorkspaceSidebar({ + projectName, + currentView, + showMemoPanel, + onViewChange, +}: WorkspaceSidebarProps) { + const isActive = (id: NavItem) => { + if (id === "memo") return showMemoPanel; + return currentView === id && !showMemoPanel; + }; + + return ( + + ); +} From 7d1469a7b6fdbf7099d143affd6522964de6900f Mon Sep 17 00:00:00 2001 From: nelee Date: Thu, 14 May 2026 13:35:19 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix(filter):=20WorkspaceFilter=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=88=84=EB=9D=BD=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #115 머지 이후 누락된 WorkspaceFilter.tsx 추가. Co-Authored-By: Claude Sonnet 4.6 --- src/components/shared/WorkspaceFilter.tsx | 92 +++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/components/shared/WorkspaceFilter.tsx diff --git a/src/components/shared/WorkspaceFilter.tsx b/src/components/shared/WorkspaceFilter.tsx new file mode 100644 index 0000000..34bd1f9 --- /dev/null +++ b/src/components/shared/WorkspaceFilter.tsx @@ -0,0 +1,92 @@ +"use client"; + +import { TaskPriority } from "@/types"; + +export interface WorkspaceFilterType { + priority: TaskPriority | "all"; + assignee: "all" | "assigned" | "unassigned" | "me"; + date: "all" | "today" | "thisWeek" | "overdue"; +} + +interface WorkspaceFilterProps { + filter: WorkspaceFilterType; + onFilterChange: (updates: Partial) => void; + taskCount: number; + totalCount: number; + onReset?: () => void; +} + +export default function WorkspaceFilter({ + filter, + onFilterChange, + taskCount, + totalCount, + onReset, +}: WorkspaceFilterProps) { + const hasActiveFilter = + filter.priority !== "all" || filter.assignee !== "all" || filter.date !== "all"; + + const btn = (active: boolean) => + `text-sm px-3 py-1.5 rounded transition-all ${ + active + ? "bg-main-500 text-white" + : "bg-card text-muted-foreground hover:bg-muted/60 hover:text-foreground" + }`; + + return ( +
+
+ {/* 우선순위 */} +
+ 우선순위 +
+ {(["all", "high", "normal", "low"] as const).map((v) => ( + + ))} +
+
+ + {/* 담당자 */} +
+ 담당자 +
+ {(["all", "me", "assigned", "unassigned"] as const).map((v) => ( + + ))} +
+
+ + {/* 기간 */} +
+ 기간 +
+ {(["all", "today", "thisWeek", "overdue"] as const).map((v) => ( + + ))} +
+
+ + {/* 결과 카운트 + 초기화 */} +
+ + {taskCount}개 / 전체 {totalCount}개 + {taskCount === 0 && hasActiveFilter && ( + — 조건에 맞는 작업이 없어요 + )} + + {hasActiveFilter && onReset && ( + + )} +
+
+
+ ); +} From 3adaaefe5c22b277b72f74eab2bdcae52c0e9d8e Mon Sep 17 00:00:00 2001 From: nelee Date: Thu, 14 May 2026 13:56:26 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat(layout):=20=EC=A0=84=EC=97=AD=20AppSid?= =?UTF-8?q?ebar=EB=A1=9C=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98?= =?UTF-8?q?=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WorkspaceNavProvider: 워크스페이스 뷰 상태를 전역 context로 관리 - AppSidebar: 루트 레이아웃에 240px 사이드바 추가 - 전역 메뉴(프로젝트, 공지사항, 어드민) + 워크스페이스 진입 시 sub-nav 표시 - 모바일(< md)에서는 숨김 - Header: 공지사항/어드민 아이콘 데스크탑에서 제거(md:hidden) - workspace 페이지: WorkspaceSidebar 제거, context 연결 - WorkspaceSidebar.tsx 삭제 Co-Authored-By: Claude Sonnet 4.6 --- .../(main)/project/workspace/[id]/page.tsx | 40 ++--- src/app/layout.tsx | 17 ++- .../features/kanban/KanbanBoard.tsx | 1 + src/components/layout/AppSidebar.tsx | 143 ++++++++++++++++++ src/components/layout/Header.tsx | 4 +- src/components/layout/WorkspaceSidebar.tsx | 98 ------------ src/providers/WorkspaceNavProvider.tsx | 65 ++++++++ 7 files changed, 243 insertions(+), 125 deletions(-) create mode 100644 src/components/layout/AppSidebar.tsx delete mode 100644 src/components/layout/WorkspaceSidebar.tsx create mode 100644 src/providers/WorkspaceNavProvider.tsx diff --git a/src/app/(main)/project/workspace/[id]/page.tsx b/src/app/(main)/project/workspace/[id]/page.tsx index 008aa2d..fe56b47 100644 --- a/src/app/(main)/project/workspace/[id]/page.tsx +++ b/src/app/(main)/project/workspace/[id]/page.tsx @@ -7,7 +7,7 @@ import dynamic from "next/dynamic"; import { queryKeys } from "@/lib/constants/queryKeys"; import BottomNavigation from "@/components/layout/BottomNavigation"; -import WorkspaceSidebar from "@/components/layout/WorkspaceSidebar"; +import { useWorkspaceNav } from "@/providers/WorkspaceNavProvider"; import { Task } from "@/types/kanban"; import { showToast } from "@/lib/utils/toast"; @@ -38,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); @@ -289,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); } }; @@ -319,14 +328,7 @@ export default function ProjectPage() { return (
-
- -
+
{/* 메인 콘텐츠 */}
@@ -337,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, @@ -364,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 */} @@ -398,9 +399,8 @@ export default function ProjectPage() { : "w-0 opacity-0" }`} > - setShowMemoPanel(false)} /> + -
diff --git a/src/app/layout.tsx b/src/app/layout.tsx index ad2694b..98d765c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,6 +5,8 @@ import { Header } from "@/components/layout/Header"; import Provider from "@/providers/providers"; import Toaster from "@/components/ui/Toaster"; import AuthProviders from "@/providers/AuthProviders"; +import { WorkspaceNavProvider } from "@/providers/WorkspaceNavProvider"; +import AppSidebar from "@/components/layout/AppSidebar"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -34,11 +36,16 @@ export default function RootLayout({ > -
-
-
{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..cff274e --- /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..62aaa4a 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -46,7 +46,7 @@ export function Header() { aria-label="프로필" /> - + - ))} - - -
- - - - - 프로젝트 목록 - -
- - ); -} 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; +} From 3bbb415a5f22dfb32f81428f996631cf6842efff Mon Sep 17 00:00:00 2001 From: nelee Date: Thu, 14 May 2026 14:00:08 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix(gnb):=20=ED=97=A4=EB=8D=94=20=EB=A1=9C?= =?UTF-8?q?=EA=B3=A0=20=EC=A1=B4=EC=9D=84=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=20=EB=84=88=EB=B9=84=EC=97=90=20=EB=A7=9E=EA=B2=8C=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 헤더를 로고 존(w-60)과 컨트롤 존으로 분리 - 데스크탑: 로고가 사이드바(w-60)와 grid 정렬, border-r로 시각적 연결 - 모바일: 기존 레이아웃 유지 - w-[240px] → w-60으로 AppSidebar/Header 통일 Co-Authored-By: Claude Sonnet 4.6 --- src/components/layout/AppSidebar.tsx | 2 +- src/components/layout/Header.tsx | 24 ++++++++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/components/layout/AppSidebar.tsx b/src/components/layout/AppSidebar.tsx index cff274e..7754cbf 100644 --- a/src/components/layout/AppSidebar.tsx +++ b/src/components/layout/AppSidebar.tsx @@ -77,7 +77,7 @@ export default function AppSidebar() { }; return ( -