From c9154da2d27848abb341d468ceaca2115459a7a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EC=9B=90?= Date: Tue, 24 Feb 2026 14:13:36 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94=20?= =?UTF-8?q?=EB=82=B4=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84,=20=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EB=B0=8F=20=EC=9E=90=EC=9C=A0=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=ED=8C=90=20=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94=20=EB=93=9C?= =?UTF-8?q?=EB=A1=AD=EB=8B=A4=EC=9A=B4=20=EC=82=AC=EC=9A=A9,=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=8B=9C=20=EC=84=B1=EA=B3=B5=20=ED=86=A0=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EB=9D=84=EC=9A=B0?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(root)/layout.tsx | 131 ++++-------------------- src/app/(root)/mypage/page.tsx | 20 +++- src/components/sidebar/MobileHeader.tsx | 45 ++++---- src/components/sidebar/Sidebar.tsx | 28 +++-- 4 files changed, 81 insertions(+), 143 deletions(-) diff --git a/src/app/(root)/layout.tsx b/src/app/(root)/layout.tsx index 36a3238..273cbca 100644 --- a/src/app/(root)/layout.tsx +++ b/src/app/(root)/layout.tsx @@ -1,28 +1,16 @@ 'use client'; -import { useState } from 'react'; import Image from 'next/image'; import { usePathname, useRouter } from 'next/navigation'; import { useCurrentUser } from '@/hooks/useCurrentUser'; -import { - Sidebar, - SidebarButton, - SidebarTeamSelect, - SidebarAddButton, - MobileHeader, - MobileDrawer, -} from '@/components/sidebar'; -import boardSmall from '@/assets/icons/board/boardSmall.svg'; -import boardLarge from '@/assets/icons/board/boardLarge.svg'; -import chessSmall from '@/assets/icons/chess/chessSmall.svg'; -import chessBig from '@/assets/icons/chess/chessBig.svg'; +import { Sidebar, MobileHeader } from '@/components/sidebar'; +import TeamSidebarDropdown from './[teamid]/_domain/components/Team/TeamSidebarDropdown'; import humanBig from '@/assets/buttons/human/humanBig.svg'; import styles from './layout.module.css'; export default function RootLayout({ children }: { children: React.ReactNode }) { const pathname = usePathname(); const router = useRouter(); - const [isDrawerOpen, setIsDrawerOpen] = useState(false); const { data: user, isPending } = useCurrentUser(); // isPending: 최초 로딩 중 (undefined) @@ -30,7 +18,6 @@ export default function RootLayout({ children }: { children: React.ReactNode }) // user !== null: 로그인 const isLoggedIn = !isPending && user !== null && user !== undefined; const isLanding = pathname === '/'; - const firstGroup = user?.memberships?.[0]?.group; // [teamid] 페이지는 자체 모바일 헤더(TeamNavClient)를 사용하므로 root layout의 MobileHeader를 숨김 const knownPaths = ['/', '/addteam', '/boards', '/mypage', '/history', '/list']; @@ -71,106 +58,30 @@ export default function RootLayout({ children }: { children: React.ReactNode }) ) } profileName={user?.nickname ?? '사용자'} - profileTeam={firstGroup?.name ?? ''} - teamSelect={(isCollapsed: boolean) => - firstGroup ? ( - !isCollapsed ? ( - - ) : ( - - ) - } - label={firstGroup.name} - isSelected - onClick={() => router.push(`/${firstGroup.id}`)} - /> - ) : ( - - ) : ( - - ) - } - label={firstGroup.name} - isActive - iconOnly - onClick={() => router.push(`/${firstGroup.id}`)} - /> - ) - ) : null - } - addButton={ + profileTeam={user?.memberships?.[0]?.group?.name ?? ''} + teamSelect={ isLoggedIn - ? (isCollapsed: boolean) => ( - <> - {!isCollapsed && ( - router.push('/addteam')} /> - )} -
- - } - label="자유게시판" - isActive - iconOnly={isCollapsed} - href="/boards" - /> - - ) + ? (isCollapsed: boolean) => : undefined } /> {!isTeamIdPage && ( - <> - - ) : undefined - } - onMenuClick={() => setIsDrawerOpen(true)} - onProfileClick={handleProfileClick} - /> - setIsDrawerOpen(false)}> - } - label="자유게시판" - isActive - href="/boards" - onClick={() => setIsDrawerOpen(false)} - /> - - + + ) : undefined + } + onProfileClick={handleProfileClick} + onLogout={handleLogout} + /> )}
{children}
diff --git a/src/app/(root)/mypage/page.tsx b/src/app/(root)/mypage/page.tsx index badc6c1..6f497c4 100644 --- a/src/app/(root)/mypage/page.tsx +++ b/src/app/(root)/mypage/page.tsx @@ -33,6 +33,7 @@ export default function ProfilePage() { } = useUser(); const [showToast, setShowToast] = useState(false); + const [successToast, setSuccessToast] = useState(null); const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false); const [isWithdrawModalOpen, setIsWithdrawModalOpen] = useState(false); const newPasswordRef = useRef(null); @@ -47,6 +48,7 @@ export default function ProfilePage() { const result = await updateProfile(); if (result.success) { setShowToast(false); + setSuccessToast('이름이 변경되었습니다.'); } }; @@ -63,6 +65,7 @@ export default function ProfilePage() { setIsPasswordModalOpen(false); if (newPasswordRef.current) newPasswordRef.current.value = ''; if (confirmPasswordRef.current) confirmPasswordRef.current.value = ''; + setSuccessToast('비밀번호가 변경되었습니다.'); } }; @@ -171,8 +174,8 @@ export default function ProfilePage() { - {hasChanges && ( -
+
+ {hasChanges && ( setShowToast(false)} className={styles.toast} /> -
- )} + )} + {successToast && ( + setSuccessToast(null)} + className={styles.toast} + /> + )} +
void; + /** 드로어 내부 콘텐츠 (미전달 시 TeamSidebarDropdown 기본 표시) */ + drawerContent?: ReactNode; /** 프로필 버튼 클릭 시 호출되는 콜백 */ onProfileClick?: () => void; /** 로그아웃 클릭 시 호출되는 콜백 */ @@ -37,16 +42,27 @@ type MobileHeaderProps = { export default function MobileHeader({ isLoggedIn, profileImage, - onMenuClick, + drawerContent, onProfileClick, onLogout, onLogoClick, logoWidth = 102, logoHeight = 20, }: MobileHeaderProps) { + const router = useRouter(); const [showProfileMenu, setShowProfileMenu] = useState(false); + const [isDrawerOpen, setIsDrawerOpen] = useState(false); const profileMenuRef = useRef(null); + const defaultLogout = useCallback(async () => { + await fetch('/api/auth/logout', { method: 'POST' }); + router.push('/login'); + }, [router]); + + const handleLogout = onLogout ?? defaultLogout; + const handleProfileClick = onProfileClick ?? (() => router.push('/mypage')); + const handleLogoClick = onLogoClick ?? (() => router.push('/addteam')); + useEffect(() => { if (!showProfileMenu) return; const handleClickOutside = (e: MouseEvent) => { @@ -61,12 +77,7 @@ export default function MobileHeader({ if (!isLoggedIn) { return (
-
+
COWORKERS
@@ -79,17 +90,12 @@ export default function MobileHeader({ -
+
COWORKERS
@@ -109,7 +115,7 @@ export default function MobileHeader({ className={styles.profileMenuItem} onClick={() => { setShowProfileMenu(false); - onProfileClick?.(); + handleProfileClick(); }} > 마이페이지 @@ -119,7 +125,7 @@ export default function MobileHeader({ className={`${styles.profileMenuItem} ${styles.profileMenuDanger}`} onClick={() => { setShowProfileMenu(false); - onLogout?.(); + handleLogout(); }} > 로그아웃 @@ -127,6 +133,9 @@ export default function MobileHeader({ )} + setIsDrawerOpen(false)}> + {drawerContent ?? } + ); } diff --git a/src/components/sidebar/Sidebar.tsx b/src/components/sidebar/Sidebar.tsx index dc814ed..8967b00 100644 --- a/src/components/sidebar/Sidebar.tsx +++ b/src/components/sidebar/Sidebar.tsx @@ -1,7 +1,8 @@ 'use client'; import type { ReactNode } from 'react'; -import { useState, useRef, useEffect } from 'react'; +import { useState, useRef, useEffect, useCallback } from 'react'; +import { useRouter } from 'next/navigation'; import Image from 'next/image'; import clsx from 'clsx'; import { motion, AnimatePresence } from 'framer-motion'; @@ -49,10 +50,20 @@ export default function Sidebar({ onLogout, onLogoClick, }: SidebarProps) { + const router = useRouter(); const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed ?? false); const [showProfileMenu, setShowProfileMenu] = useState(false); const profileMenuRef = useRef(null); + const defaultLogout = useCallback(async () => { + await fetch('/api/auth/logout', { method: 'POST' }); + router.push('/login'); + }, [router]); + + const handleLogout = onLogout ?? defaultLogout; + const handleProfileClick = onProfileClick ?? (() => router.push('/mypage')); + const handleLogoClick = onLogoClick ?? (() => router.push('/addteam')); + useEffect(() => { if (!showProfileMenu) return; const handleClickOutside = (e: MouseEvent) => { @@ -72,7 +83,7 @@ export default function Sidebar({ const renderFooter = () => { if (footer) { return ( -
+
{renderSlot(footer)}
); @@ -80,7 +91,7 @@ export default function Sidebar({ if (!isLoggedIn) { return ( - + {!isCollapsed && ( { setShowProfileMenu(false); - onProfileClick?.(); + handleProfileClick(); }} > 마이페이지 @@ -120,7 +131,7 @@ export default function Sidebar({ className={`${styles.profileMenuItem} ${styles.profileMenuDanger}`} onClick={() => { setShowProfileMenu(false); - onLogout?.(); + handleLogout(); }} > 로그아웃 @@ -155,12 +166,7 @@ export default function Sidebar({ transition={{ duration: 0.3, ease: 'easeInOut' }} >
-
+
{isCollapsed ? ( Date: Tue, 24 Feb 2026 14:49:52 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=EC=A0=9C=EB=AF=B8=EB=82=98=EC=9D=B4?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(root)/layout.tsx | 67 +++++++++++++------------ src/components/sidebar/MobileHeader.tsx | 11 ++-- 2 files changed, 42 insertions(+), 36 deletions(-) diff --git a/src/app/(root)/layout.tsx b/src/app/(root)/layout.tsx index 273cbca..e32fcef 100644 --- a/src/app/(root)/layout.tsx +++ b/src/app/(root)/layout.tsx @@ -19,9 +19,11 @@ export default function RootLayout({ children }: { children: React.ReactNode }) const isLoggedIn = !isPending && user !== null && user !== undefined; const isLanding = pathname === '/'; - // [teamid] 페이지는 자체 모바일 헤더(TeamNavClient)를 사용하므로 root layout의 MobileHeader를 숨김 - const knownPaths = ['/', '/addteam', '/boards', '/mypage', '/history', '/list']; - const isTeamIdPage = !knownPaths.some((p) => pathname === p || pathname.startsWith(p + '/')); + // 자체 사이드바가 없는 페이지에서만 root layout 사이드바 표시 + const rootSidebarPaths = ['/', '/boards', '/mypage']; + const showRootSidebar = rootSidebarPaths.some( + (p) => pathname === p || pathname.startsWith(p + '/'), + ); const handleProfileClick = () => { if (isLoggedIn) { @@ -38,34 +40,36 @@ export default function RootLayout({ children }: { children: React.ReactNode }) return (
- router.push('/addteam')} - profileImage={ - user?.image ? ( - - ) : ( - - ) - } - profileName={user?.nickname ?? '사용자'} - profileTeam={user?.memberships?.[0]?.group?.name ?? ''} - teamSelect={ - isLoggedIn - ? (isCollapsed: boolean) => - : undefined - } - /> - {!isTeamIdPage && ( + {showRootSidebar && ( + router.push('/addteam')} + profileImage={ + user?.image ? ( + + ) : ( + + ) + } + profileName={user?.nickname ?? '사용자'} + profileTeam={user?.memberships?.[0]?.group?.name ?? ''} + teamSelect={ + isLoggedIn + ? (isCollapsed: boolean) => + : undefined + } + /> + )} + {showRootSidebar && ( } /> )}
{children}
diff --git a/src/components/sidebar/MobileHeader.tsx b/src/components/sidebar/MobileHeader.tsx index 3d1dfbe..c2ccd02 100644 --- a/src/components/sidebar/MobileHeader.tsx +++ b/src/components/sidebar/MobileHeader.tsx @@ -5,7 +5,6 @@ import { useRouter } from 'next/navigation'; import Image from 'next/image'; import clsx from 'clsx'; import MobileDrawer from './MobileDrawer'; -import TeamSidebarDropdown from '@/app/(root)/[teamid]/_domain/components/Team/TeamSidebarDropdown'; import styles from './styles/MobileHeader.module.css'; import logoSmall from '@/assets/logos/logoSmall.svg'; @@ -20,7 +19,7 @@ type MobileHeaderProps = { profileImage?: ReactNode; /** @deprecated 내부 드로어로 대체됨. 호환성을 위해 유지 */ onMenuClick?: () => void; - /** 드로어 내부 콘텐츠 (미전달 시 TeamSidebarDropdown 기본 표시) */ + /** 드로어 내부 콘텐츠 (전달 시 햄버거 메뉴 클릭으로 드로어 표시) */ drawerContent?: ReactNode; /** 프로필 버튼 클릭 시 호출되는 콜백 */ onProfileClick?: () => void; @@ -133,9 +132,11 @@ export default function MobileHeader({
)}
- setIsDrawerOpen(false)}> - {drawerContent ?? } - + {drawerContent && ( + setIsDrawerOpen(false)}> + {drawerContent} + + )} ); }