diff --git a/src/app/(main)/goal/notes/page.tsx b/src/app/(main)/goal/notes/page.tsx index b690c734..24b3866e 100644 --- a/src/app/(main)/goal/notes/page.tsx +++ b/src/app/(main)/goal/notes/page.tsx @@ -1,4 +1,11 @@ +'use client'; + +import React, { useState } from 'react'; + +import { useNoteSidebar } from '@/app/providers/NoteSidebarProvider'; import NotesClient from '@/components/notes/NotesClient'; +import NoteSidebar from '@/components/ui/NoteSidebar/NoteSidebar'; +import { TodoWithNotes } from '@/interfaces/todo'; import { cn } from '@/lib/utils'; interface NotesPageProps { @@ -7,14 +14,55 @@ interface NotesPageProps { }>; } -const NotesPage = async ({ searchParams }: NotesPageProps) => { - const params = await searchParams; +const NotesPage = ({ searchParams }: NotesPageProps) => { + const params = React.use(searchParams); const goalId = params.goalId ? Number(params.goalId) : undefined; + const { setIsNoteSidebarOpen } = useNoteSidebar(); // 사이드바 열림 여부 + const [selectedTodo, setSelectedTodo] = useState(null); + + const handleTodoClick = (todo: TodoWithNotes) => { + setSelectedTodo(todo); + setIsNoteSidebarOpen(true); + }; + + const handleCloseSidebar = () => { + setIsNoteSidebarOpen(false); + setSelectedTodo(null); + }; + return ( -
- -
+ <> +
+ +
+ + {/* 데스크탑 사이드바 */} + {selectedTodo && ( +
+ +
+ )} + + {/* 모바일/태블릿 사이드바 */} + {selectedTodo && ( +
+ +
+ )} + ); }; diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index 54f3c10f..ffc2f49b 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -2,13 +2,16 @@ import { usePathname } from 'next/navigation'; +import { NoteSidebarProvider, useNoteSidebar } from '@/app/providers/NoteSidebarProvider'; import { SidebarProvider } from '@/app/providers/SidebarProvider'; import Sidebar from '@/components/sidebar/Sidebar'; export default function MainLayout({ children }: { children: React.ReactNode }) { return ( - {children} + + {children} + ); } @@ -29,10 +32,18 @@ function SidebarLayout({ children }: { children: React.ReactNode }) { } function MainContent({ children }: { children: React.ReactNode }) { + const { isNoteSidebarOpen } = useNoteSidebar(); + return (
-
{children}
+
+ {children} +
); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index ce948891..dbae5953 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -62,7 +62,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo return ( - + {children} diff --git a/src/app/providers/NoteSidebarProvider.tsx b/src/app/providers/NoteSidebarProvider.tsx new file mode 100644 index 00000000..e7aff2f3 --- /dev/null +++ b/src/app/providers/NoteSidebarProvider.tsx @@ -0,0 +1,28 @@ +'use client'; + +import { createContext, ReactNode, useContext, useState } from 'react'; + +interface NoteSidebarContextType { + isNoteSidebarOpen: boolean; + setIsNoteSidebarOpen: (value: boolean) => void; +} + +const NoteSidebarContext = createContext(undefined); + +export const useNoteSidebar = () => { + const context = useContext(NoteSidebarContext); + if (!context) { + throw new Error('useNoteSidebar must be used within a NoteSidebarProvider'); + } + return context; +}; + +export const NoteSidebarProvider = ({ children }: { children: ReactNode }) => { + const [isNoteSidebarOpen, setIsNoteSidebarOpen] = useState(false); + + return ( + + {children} + + ); +}; diff --git a/src/components/goals/goalDetail/GoalDetailClient.tsx b/src/components/goals/goalDetail/GoalDetailClient.tsx index dcc46b5e..19ec27b7 100644 --- a/src/components/goals/goalDetail/GoalDetailClient.tsx +++ b/src/components/goals/goalDetail/GoalDetailClient.tsx @@ -67,7 +67,7 @@ const GoalDetailClient = ({ goalId }: GoalDetailClientProps) => { } return ( -
+
{/* 목표 정보 헤더 */}
diff --git a/src/components/notes/NotesClient.tsx b/src/components/notes/NotesClient.tsx index ac3f730c..bbf9f62a 100644 --- a/src/components/notes/NotesClient.tsx +++ b/src/components/notes/NotesClient.tsx @@ -4,7 +4,6 @@ import { useMemo, useState } from 'react'; import TodoWithNoteList from '@/components/notes/TodoWithNoteList'; import GoalSelector from '@/components/todos/GoalSelector'; -import NoteSidebar from '@/components/ui/NoteSidebar/NoteSidebar'; import Pagination from '@/components/ui/Pagination'; import { useTodosWithNotes } from '@/hooks/useNotes'; import { TodoWithNotes } from '@/interfaces/todo'; @@ -13,19 +12,17 @@ const ITEMS_PER_PAGE = 6; // 페이지당 표시할 할 일 개수 interface NotesClientProps { initialGoalId?: number; + onTodoClick: (todo: TodoWithNotes) => void; } -const NotesClient = ({ initialGoalId }: NotesClientProps) => { +const NotesClient = ({ initialGoalId, onTodoClick }: NotesClientProps) => { const [selectedGoalId, setSelectedGoalId] = useState(initialGoalId || 0); - const [isSidebarOpen, setIsSidebarOpen] = useState(false); - const [selectedTodo, setSelectedTodo] = useState(null); const [currentPage, setCurrentPage] = useState(1); const { data: allTodosWithNotes = [] } = useTodosWithNotes( selectedGoalId === 0 ? undefined : selectedGoalId, ); - // 페이지네이션 계산 const paginatedData = useMemo(() => { const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; const endIndex = startIndex + ITEMS_PER_PAGE; @@ -42,28 +39,17 @@ const NotesClient = ({ initialGoalId }: NotesClientProps) => { }; }, [allTodosWithNotes.length, currentPage]); - const handleTodoClick = (todo: TodoWithNotes) => { - setSelectedTodo(todo); - setIsSidebarOpen(true); - }; - - const handleCloseSidebar = () => { - setIsSidebarOpen(false); - setSelectedTodo(null); - }; - const handlePageChange = (page: number) => { setCurrentPage(page); }; - // 목표 변경 시 페이지 초기화 const handleGoalChange = (goalId: number) => { setSelectedGoalId(goalId); setCurrentPage(1); }; return ( -
+

@@ -72,38 +58,25 @@ const NotesClient = ({ initialGoalId }: NotesClientProps) => {

-
-
-
- -
- - - {/* 페이지네이션 */} - {paginationInfo.totalPages > 1 && ( - - )} -
-
- - {/* 할 일 클릭 시 열리는 사이드바 */} - {isSidebarOpen && selectedTodo && ( - - )} +
+
+ +
+ + + {paginationInfo.totalPages > 1 && ( + + )} +
); }; diff --git a/src/components/todos/GoalSelector.tsx b/src/components/todos/GoalSelector.tsx index 3f5df7dd..ac223f62 100644 --- a/src/components/todos/GoalSelector.tsx +++ b/src/components/todos/GoalSelector.tsx @@ -112,6 +112,8 @@ const GoalSelector = ({ size="todo" animation="slide" shadow="md" + matchTriggerWidth + offsetPx={2} className="max-h-200 w-full overflow-y-auto" >
diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index cf78d871..f50443ff 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -49,7 +49,7 @@ const buttonVariants = cva('flex cursor-pointer items-center justify-center', { schedule: 'h-48 w-120 sm:w-165.5', scheduleDashboard: 'h-40 w-120 md:w-160', tempNote: 'h-40 w-84', - sideNote: 'h-48 w-260', + sideNote: 'h-40 w-100 md:h-48 md:w-200 lg:w-260', error: 'h-44 w-200', eula: 'h-48 w-520', }, diff --git a/src/components/ui/DropdownMenu.tsx b/src/components/ui/DropdownMenu.tsx index 27fb010f..189142ab 100644 --- a/src/components/ui/DropdownMenu.tsx +++ b/src/components/ui/DropdownMenu.tsx @@ -25,7 +25,7 @@ const dropdownVariants = cva('rounded-b-[20px] border shadow-lg', { lg: 'max-w-md min-w-64', auto: 'w-auto max-w-xs', goalListFilter: 'max-w-148', - todo: 'max-w-520', + todo: 'max-w-1296', full: 'w-auto', }, animation: { @@ -56,6 +56,7 @@ export interface DropdownMenuProps extends VariantProps position?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end'; matchTriggerWidth?: boolean; zIndex?: number; + offsetPx?: number; } const DropdownMenu = ({ @@ -70,6 +71,7 @@ const DropdownMenu = ({ position = 'bottom-start', matchTriggerWidth = false, zIndex = 9999, + offsetPx, }: DropdownMenuProps) => { // Floating UI 설정 const { refs, floatingStyles, context } = useFloating({ @@ -78,7 +80,7 @@ const DropdownMenu = ({ if (!open) onClose(); }, middleware: [ - offset(16), // 16px 간격 + offset(offsetPx), flip(), // 화면 경계에 닿으면 위치 자동 조정 shift(), // 화면 밖으로 나가지 않도록 이동 ...(matchTriggerWidth @@ -87,6 +89,7 @@ const DropdownMenu = ({ apply({ rects, elements }) { Object.assign(elements.floating.style, { width: `${rects.reference.width}px`, + boxSizing: 'border-box', }); }, }), diff --git a/src/components/ui/NoteSidebar/NoteDetailReadMode.tsx b/src/components/ui/NoteSidebar/NoteDetailReadMode.tsx index c6368207..c9aaed38 100644 --- a/src/components/ui/NoteSidebar/NoteDetailReadMode.tsx +++ b/src/components/ui/NoteSidebar/NoteDetailReadMode.tsx @@ -40,7 +40,8 @@ const NoteDetailReadMode = ({ note, onEdit, goalTitle, todoTitle }: NoteDetailRe className="bg-primary-01 text-white" icon={} > - 수정하기 + 수정 + 수정하기
diff --git a/src/components/ui/NoteSidebar/NoteSidebar.tsx b/src/components/ui/NoteSidebar/NoteSidebar.tsx index 4ac1e962..5d6886e3 100644 --- a/src/components/ui/NoteSidebar/NoteSidebar.tsx +++ b/src/components/ui/NoteSidebar/NoteSidebar.tsx @@ -10,55 +10,85 @@ interface NoteSidebarProps { todo?: TodoWithNotes; goalTitle?: string; onClose: () => void; + isDesktop?: boolean; } -const NoteSidebar = ({ isOpen, todo, goalTitle, onClose }: NoteSidebarProps) => { +// 2단계 레이어 구조: NoteListSidebar → NoteDetailSidebar +const NoteSidebar = ({ isOpen, todo, goalTitle, onClose, isDesktop }: NoteSidebarProps) => { const [currentView, setCurrentView] = useState<'note-list' | 'note-detail'>('note-list'); - const [setselectedNoteId, setsetselectedNoteId] = useState(null); + const [selectedNoteId, setSelectedNoteId] = useState(null); if (!isOpen) return null; const handleNoteClick = (noteId: number) => { setCurrentView('note-detail'); - setsetselectedNoteId(noteId); + setSelectedNoteId(noteId); }; const handleBack = () => { setCurrentView('note-list'); + setSelectedNoteId(null); }; const handleClose = () => { setCurrentView('note-list'); + setSelectedNoteId(null); onClose(); }; return ( <> - {/* 배경 오버레이 */} -
+ {/* ===== 첫 번째 레이어: NoteListSidebar ===== */} + {currentView === 'note-list' && ( + <> + {/* 데스크탑: fixed 우측 */} + {isDesktop && ( +
+ {todo && ( + + )} +
+ )} - {/* 사이드바 */} -
- {currentView === 'note-list' && todo && ( - - )} - {currentView === 'note-detail' && setselectedNoteId && todo && ( - - )} -
+ {/* 모바일/태블릿: fixed 우측 + 오버레이 */} + {!isDesktop && ( + <> +
+
+ {todo && ( + + )} +
+ + )} + + )} + + {/* ===== 두 번째 레이어: NoteDetailSidebar (모달) ===== */} + {currentView === 'note-detail' && selectedNoteId && ( + <> + {/* 배경 오버레이 - NoteListSidebar 위를 어둡게 */} +
+ + {/* NoteDetailSidebar 모달 */} +
+ {todo && ( + + )} +
+ + )} ); };