o2o-castad-frontend/src/components/Sidebar.tsx

194 lines
7.5 KiB
TypeScript
Executable File

import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { UserMeResponse } from '../types/api';
import { logout } from '../utils/api';
import LanguageSwitch from './LanguageSwitch';
interface SidebarItemProps {
icon: React.ReactNode;
label: string;
isActive?: boolean;
isCollapsed: boolean;
isDisabled?: boolean;
onClick?: () => void;
}
const SidebarItem: React.FC<SidebarItemProps> = ({ icon, label, isActive, isCollapsed, isDisabled, onClick }) => {
return (
<div
onClick={isDisabled ? undefined : onClick}
className={`sidebar-item ${isActive ? 'active' : ''} ${isCollapsed ? 'collapsed' : ''} ${isDisabled ? 'disabled' : ''}`}
title={isCollapsed ? label : ""}
>
<div className="sidebar-item-icon">
{icon}
</div>
{!isCollapsed && <span className="sidebar-item-label">{label}</span>}
</div>
);
};
interface SidebarProps {
activeItem: string;
onNavigate: (id: string) => void;
onHome?: () => void;
userInfo?: UserMeResponse | null;
onLogout?: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ activeItem, onNavigate, onHome, userInfo, onLogout }) => {
const { t } = useTranslation();
const [isCollapsed, setIsCollapsed] = useState(false);
const [isMobileOpen, setIsMobileOpen] = useState(false);
const [isLoggingOut, setIsLoggingOut] = useState(false);
// 로그아웃 처리
const handleLogout = async () => {
if (isLoggingOut) return;
setIsLoggingOut(true);
try {
await logout();
onLogout?.();
} catch (error) {
console.error('Logout failed:', error);
// 에러가 나도 로컬 토큰은 이미 삭제됨, 홈으로 이동
onLogout?.();
} finally {
setIsLoggingOut(false);
}
};
useEffect(() => {
const handleResize = () => {
if (window.innerWidth < 768) {
setIsMobileOpen(false);
}
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
const handleNavigate = (id: string) => {
onNavigate(id);
if (window.innerWidth < 768) {
setIsMobileOpen(false);
}
};
const menuItems = [
{ id: '대시보드', label: t('sidebar.dashboard'), disabled: false, icon: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/><rect x="14" y="12" width="7" height="9"/><rect x="3" y="16" width="7" height="5"/></svg> },
{ id: '새 프로젝트 만들기', label: t('sidebar.newProject'), disabled: false, icon: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> },
{ id: 'ADO2 콘텐츠', label: t('sidebar.ado2Contents'), disabled: false, icon: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg> },
{ id: '내 콘텐츠', label: t('sidebar.myContents'), disabled: true, icon: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg> },
{ id: '내 정보', label: t('sidebar.myInfo'), disabled: false, icon: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg> },
];
return (
<>
{/* Mobile Menu Button */}
<button
onClick={() => setIsMobileOpen(true)}
className="mobile-menu-btn"
>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/>
</svg>
</button>
{/* Mobile Overlay */}
{isMobileOpen && (
<div
className="mobile-overlay"
onClick={() => setIsMobileOpen(false)}
/>
)}
{/* Sidebar */}
<div className={`sidebar ${isCollapsed ? 'collapsed' : 'expanded'} ${isMobileOpen ? 'mobile-open' : 'mobile-closed'}`}>
<div className={`sidebar-header ${isCollapsed ? 'collapsed' : ''}`}>
{!isCollapsed && (
<img
onClick={onHome}
src="/assets/images/ado2-sidebar-logo.svg"
alt="ADO2"
className="sidebar-logo"
/>
)}
<button
onClick={() => {
if (window.innerWidth < 768) {
setIsMobileOpen(false);
} else {
setIsCollapsed(!isCollapsed);
}
}}
className="p-1.5 text-gray-400 hover:text-white"
>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/>
</svg>
</button>
</div>
<div className="sidebar-menu no-scrollbar">
{menuItems.map(item => (
<SidebarItem
key={item.id}
icon={item.icon}
label={item.label}
isCollapsed={isCollapsed}
isActive={activeItem === item.id}
isDisabled={item.disabled}
onClick={() => handleNavigate(item.id)}
/>
))}
</div>
<div className="sidebar-footer">
<div className="sidebar-language-switch">
<LanguageSwitch isCollapsed={isCollapsed} />
</div>
<div className={`profile-section ${isCollapsed ? 'collapsed' : ''}`}>
{userInfo?.profile_image_url || userInfo?.thumbnail_image_url ? (
<img
src={userInfo.thumbnail_image_url || userInfo.profile_image_url || ''}
alt="Profile"
className="profile-avatar"
/>
) : (
<div className="profile-avatar profile-avatar-default">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
</div>
)}
{!isCollapsed && (
<div className="flex-1 min-w-0">
<p className="profile-name">{userInfo?.nickname || t('sidebar.defaultUser')}</p>
</div>
)}
</div>
<button
className={`logout-btn ${isCollapsed ? 'collapsed' : ''}`}
onClick={handleLogout}
disabled={isLoggingOut}
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/>
</svg>
{!isCollapsed && <span className="logout-btn-label">{isLoggingOut ? t('sidebar.loggingOut') : t('sidebar.logout')}</span>}
</button>
</div>
</div>
</>
);
};
export default Sidebar;