194 lines
7.5 KiB
TypeScript
Executable File
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;
|