Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 21 additions & 12 deletions src/app/(main)/project/workspace/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -37,10 +38,10 @@ export default function ProjectPage() {
const queryClient = useQueryClient();

const [kanbanBoardId, setKanbanBoardId] = useState<string>("");
const [currentView, setCurrentView] = useState<NavItem>("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);

Expand Down Expand Up @@ -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);
}
};

Expand All @@ -318,7 +328,7 @@ export default function ProjectPage() {

return (
<div className="flex flex-col h-full bg-background">
<div className="flex-1 flex overflow-hidden min-h-0 gap-2 lg:gap-3 p-2 sm:p-3 w-full">
<div className="flex-1 flex overflow-hidden min-h-0 gap-2 lg:gap-3 p-2 sm:p-3">

{/* 메인 콘텐츠 */}
<main className="flex-1 overflow-hidden min-h-0">
Expand All @@ -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,
Expand All @@ -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(); }}
/>
)}

</main>

{/* 프로젝트 정보 side panel */}
Expand Down Expand Up @@ -390,11 +399,11 @@ export default function ProjectPage() {
: "w-0 opacity-0"
}`}
>
<MemoView projectId={projectId} onClose={()=>setShowMemoPanel(false)} />
<MemoView projectId={projectId} onClose={closeMemo} />
</aside>
</div>

<div className="shrink-0">
<div className="shrink-0 md:hidden">
<BottomNavigation
activeView={showMemoPanel ? "memo" : currentView}
onViewChange={handleViewChange}
Expand Down
17 changes: 12 additions & 5 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -34,11 +36,16 @@ export default function RootLayout({
>
<Provider>
<AuthProviders>
<div className="h-full flex flex-col">
<Header />
<div className="flex-1 min-w-0 overflow-auto scrollbar-gutter-stable pt-16">{children}</div>
<Toaster />
</div>
<WorkspaceNavProvider>
<div className="h-full flex flex-col">
<Header />
<div className="flex-1 min-w-0 flex overflow-hidden pt-16">
<AppSidebar />
<div className="flex-1 overflow-auto scrollbar-gutter-stable">{children}</div>
</div>
<Toaster />
</div>
</WorkspaceNavProvider>
</AuthProviders>
</Provider>
</body>
Expand Down
1 change: 1 addition & 0 deletions src/components/features/kanban/KanbanBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const KanbanBoard = ({
onSelectTask,
onTaskCreated,
onProjectInfoClick,
onPanelOpen,
showMemoPanel,
onToggleMemo,
}: KanbanBoardProps) => {
Expand Down
143 changes: 143 additions & 0 deletions src/components/layout/AppSidebar.tsx
Original file line number Diff line number Diff line change
@@ -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: (
<svg className="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M3 7a2 2 0 012-2h4a2 2 0 012 2v4a2 2 0 01-2 2H5a2 2 0 01-2-2V7zM13 7a2 2 0 012-2h4a2 2 0 012 2v4a2 2 0 01-2 2h-4a2 2 0 01-2-2V7zM3 17a2 2 0 012-2h4a2 2 0 012 2v2a2 2 0 01-2 2H5a2 2 0 01-2-2v-2zM13 17a2 2 0 012-2h4a2 2 0 012 2v2a2 2 0 01-2 2h-4a2 2 0 01-2-2v-2z" />
</svg>
),
},
{
href: "/notice",
label: "공지사항",
icon: (
<svg className="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0 100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234 9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z" />
</svg>
),
},
];

const workspaceNavItems = [
{
id: "kanban" as const,
label: "칸반보드",
icon: (
<svg className="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
</svg>
),
},
{
id: "calendar" as const,
label: "캘린더",
icon: (
<svg className="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
),
},
{
id: "memo" as const,
label: "메모",
icon: (
<svg className="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
),
},
];

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 (
<aside className="hidden md:flex w-60 shrink-0 flex-col border-r border-border bg-card overflow-y-auto">
<nav className="p-2 space-y-0.5">
<p className="px-3 py-2 text-xs font-medium text-muted-foreground uppercase tracking-wider">메뉴</p>

{globalNavItems.map((item) => (
<Link
key={item.href}
href={item.href}
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors ${
isGlobalActive(item.href)
? "bg-main-500/10 text-main-700 dark:text-main-300 font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
}`}
>
{item.icon}
<span>{item.label}</span>
</Link>
))}

{session?.user?.role === "admin" && (
<Link
href="/admin?tabs=users"
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors ${
pathname.startsWith("/admin")
? "bg-main-500/10 text-main-700 dark:text-main-300 font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
}`}
>
<svg className="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
<span>관리자</span>
</Link>
)}
</nav>

{isActive && (
<>
<div className="mx-3 border-t border-border" />
<nav className="p-2 space-y-0.5">
<p className="px-3 py-2 text-xs font-medium text-muted-foreground uppercase tracking-wider truncate">
{projectName}
</p>
{workspaceNavItems.map((item) => (
<button
key={item.id}
onClick={() => item.id === "memo" ? toggleMemo() : setView(item.id)}
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors ${
isWorkspaceItemActive(item.id)
? "bg-main-500/10 text-main-700 dark:text-main-300 font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
}`}
>
{item.icon}
<span>{item.label}</span>
</button>
))}
</nav>
</>
)}
</aside>
);
}
28 changes: 20 additions & 8 deletions src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,25 @@ export function Header() {
return (
<>
<header className="fixed top-0 left-0 right-0 z-50 w-full border-b border-border bg-card">
<div className="w-full max-w-7xl px-8 mx-auto h-16 flex items-center justify-between">
<Link href="/" className="flex flex-col items-start leading-none gap-0.5">
<span className="font-bold text-2xl text-main-500 dark:text-main-400 leading-none">Taskry</span>
<span className="text-[13px] text-main-600 dark:text-main-300 leading-snug">프로젝트 관리 플랫폼</span>
</Link>
<div className="w-full h-16 flex items-center">
{/* 로고 존 — 사이드바 너비와 일치 (데스크탑) */}
<div className="hidden md:flex w-60 shrink-0 items-center px-4 h-full border-r border-border">
<Link href="/" className="flex flex-col items-start leading-none gap-0.5">
<span className="font-bold text-2xl text-main-500 dark:text-main-400 leading-none">Taskry</span>
<span className="text-[13px] text-main-600 dark:text-main-300 leading-snug">프로젝트 관리 플랫폼</span>
</Link>
</div>

{/* 로고 (모바일) */}
<div className="md:hidden px-6">
<Link href="/" className="flex flex-col items-start leading-none gap-0.5">
<span className="font-bold text-2xl text-main-500 dark:text-main-400 leading-none">Taskry</span>
<span className="text-[13px] text-main-600 dark:text-main-300 leading-snug">프로젝트 관리 플랫폼</span>
</Link>
</div>

<div className="flex items-center gap-2">
{/* 우측 컨트롤 */}
<div className="flex-1 flex items-center justify-end px-4 gap-2">
<Button
btnType="icon"
icon="userCircle"
Expand All @@ -46,7 +58,7 @@ export function Header() {
aria-label="프로필"
/>

<Link href="/notice">
<Link href="/notice" className="md:hidden">
<Button
btnType="icon"
icon="speakerphone"
Expand All @@ -57,7 +69,7 @@ export function Header() {
</Link>

{session?.user?.role === "admin" && (
<Link href="/admin?tabs=users">
<Link href="/admin?tabs=users" className="md:hidden">
<Button
btnType="icon"
icon="crown"
Expand Down
Loading
Loading