ERP-node/frontend/components/layout/AppLayout.tsx

776 lines
29 KiB
TypeScript
Raw Normal View History

2025-08-26 17:20:45 +09:00
"use client";
import { useState, Suspense, useEffect } from "react";
2025-09-04 17:01:07 +09:00
import { useRouter, usePathname, useSearchParams } from "next/navigation";
2025-08-26 17:20:45 +09:00
import { Button } from "@/components/ui/button";
import {
Shield,
Menu,
Home,
Settings,
BarChart3,
FileText,
Users,
Package,
ChevronDown,
ChevronRight,
UserCheck,
2025-12-03 10:03:24 +09:00
LogOut,
User,
Building2,
2025-08-26 17:20:45 +09:00
} from "lucide-react";
import { useMenu } from "@/contexts/MenuContext";
import { useAuth } from "@/hooks/useAuth";
import { useProfile } from "@/hooks/useProfile";
import { MenuItem } from "@/lib/api/menu";
2025-09-01 18:42:59 +09:00
import { menuScreenApi } from "@/lib/api/screen";
import { apiClient } from "@/lib/api/client";
2025-09-01 18:42:59 +09:00
import { toast } from "sonner";
2025-08-26 17:20:45 +09:00
import { ProfileModal } from "./ProfileModal";
2025-12-03 10:03:24 +09:00
import { Logo } from "./Logo";
import { SideMenu } from "./SideMenu";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
import { CompanySwitcher } from "@/components/admin/CompanySwitcher";
2025-08-26 17:20:45 +09:00
// useAuth의 UserInfo 타입을 확장
interface ExtendedUserInfo {
userId: string;
userName: string;
userNameEng?: string;
userNameCn?: string;
deptCode?: string;
deptName?: string;
positionCode?: string;
positionName?: string;
email?: string;
tel?: string;
cellPhone?: string;
userType?: string;
userTypeName?: string;
authName?: string;
partnerCd?: string;
isAdmin: boolean;
sabun?: string;
photo?: string | null;
companyCode?: string;
locale?: string;
}
interface AppLayoutProps {
children: React.ReactNode;
}
// 메뉴 아이콘 매핑 함수
const getMenuIcon = (menuName: string) => {
const name = menuName.toLowerCase();
if (name.includes("대시보드") || name.includes("dashboard")) return <Home className="h-4 w-4" />;
if (name.includes("관리자") || name.includes("admin")) return <Shield className="h-4 w-4" />;
if (name.includes("사용자") || name.includes("user")) return <Users className="h-4 w-4" />;
if (name.includes("프로젝트") || name.includes("project")) return <BarChart3 className="h-4 w-4" />;
if (name.includes("제품") || name.includes("product")) return <Package className="h-4 w-4" />;
if (name.includes("설정") || name.includes("setting")) return <Settings className="h-4 w-4" />;
if (name.includes("로그") || name.includes("log")) return <FileText className="h-4 w-4" />;
if (name.includes("메뉴") || name.includes("menu")) return <Menu className="h-4 w-4" />;
2025-09-01 11:48:12 +09:00
if (name.includes("화면관리") || name.includes("screen")) return <FileText className="h-4 w-4" />;
2025-08-26 17:20:45 +09:00
return <FileText className="h-4 w-4" />;
};
// 메뉴 데이터를 UI용으로 변환하는 함수 (최상위 "사용자", "관리자" 제외)
const convertMenuToUI = (menus: MenuItem[], userInfo: ExtendedUserInfo | null, parentId: string = "0"): any[] => {
const filteredMenus = menus
.filter((menu) => (menu.parent_obj_id || menu.PARENT_OBJ_ID) === parentId)
.filter((menu) => (menu.status || menu.STATUS) === "active")
.sort((a, b) => (a.seq || a.SEQ || 0) - (b.seq || b.SEQ || 0));
// 최상위 레벨에서 "사용자", "관리자" 카테고리가 있으면 그 하위 메뉴들을 직접 표시
if (parentId === "0") {
const allMenus: any[] = [];
for (const menu of filteredMenus) {
const menuName = (menu.menu_name_kor || menu.MENU_NAME_KOR || "").toLowerCase();
// "사용자" 또는 "관리자" 카테고리면 하위 메뉴들을 직접 추가
if (menuName.includes("사용자") || menuName.includes("관리자")) {
const childMenus = convertMenuToUI(menus, userInfo, menu.objid || menu.OBJID);
allMenus.push(...childMenus);
} else {
// 일반 메뉴는 그대로 추가
allMenus.push(convertSingleMenu(menu, menus, userInfo));
}
}
return allMenus;
}
return filteredMenus.map((menu) => convertSingleMenu(menu, menus, userInfo));
};
// 단일 메뉴 변환 함수
const convertSingleMenu = (menu: MenuItem, allMenus: MenuItem[], userInfo: ExtendedUserInfo | null): any => {
const menuId = menu.objid || menu.OBJID;
// 사용자 locale 기준으로 번역 처리
const getDisplayText = (menu: MenuItem) => {
// 다국어 텍스트가 있으면 사용, 없으면 기본 텍스트 사용
if (menu.translated_name || menu.TRANSLATED_NAME) {
return menu.translated_name || menu.TRANSLATED_NAME;
}
const baseName = menu.menu_name_kor || menu.MENU_NAME_KOR || "메뉴명 없음";
// 사용자 정보에서 locale 가져오기
const userLocale = userInfo?.locale || "ko";
if (userLocale === "EN") {
// 영어 번역
const translations: { [key: string]: string } = {
: "Administrator",
: "User Management",
: "Menu Management",
: "Dashboard",
: "Permission Management",
: "Code Management",
: "Settings",
: "Log Management",
: "Project Management",
: "Product Management",
};
for (const [korean, english] of Object.entries(translations)) {
if (baseName.includes(korean)) {
return baseName.replace(korean, english);
}
}
} else if (userLocale === "JA") {
// 일본어 번역
const translations: { [key: string]: string } = {
: "管理者",
: "ユーザー管理",
: "メニュー管理",
: "ダッシュボード",
: "権限管理",
: "コード管理",
: "設定",
: "ログ管理",
: "プロジェクト管理",
: "製品管理",
};
for (const [korean, japanese] of Object.entries(translations)) {
if (baseName.includes(korean)) {
return baseName.replace(korean, japanese);
}
}
} else if (userLocale === "ZH") {
// 중국어 번역
const translations: { [key: string]: string } = {
: "管理员",
: "用户管理",
: "菜单管理",
: "仪表板",
: "权限管理",
: "代码管理",
: "设置",
: "日志管理",
: "项目管理",
: "产品管理",
};
for (const [korean, chinese] of Object.entries(translations)) {
if (baseName.includes(korean)) {
return baseName.replace(korean, chinese);
}
}
}
return baseName;
};
const children = convertMenuToUI(allMenus, userInfo, menuId);
return {
id: menuId,
name: getDisplayText(menu),
icon: getMenuIcon(menu.menu_name_kor || menu.MENU_NAME_KOR || ""),
url: menu.menu_url || menu.MENU_URL || "#",
children: children.length > 0 ? children : undefined,
hasChildren: children.length > 0,
};
};
2025-09-05 14:00:45 +09:00
function AppLayoutInner({ children }: AppLayoutProps) {
2025-08-26 17:20:45 +09:00
const router = useRouter();
const pathname = usePathname();
2025-09-04 17:01:07 +09:00
const searchParams = useSearchParams();
const { user, logout, refreshUserData, switchCompany } = useAuth();
const { userMenus, adminMenus, loading, refreshMenus } = useMenu();
const [sidebarOpen, setSidebarOpen] = useState(true);
2025-08-26 17:20:45 +09:00
const [expandedMenus, setExpandedMenus] = useState<Set<string>>(new Set());
const [isMobile, setIsMobile] = useState(false);
const [showCompanySwitcher, setShowCompanySwitcher] = useState(false);
const [currentCompanyName, setCurrentCompanyName] = useState<string>("");
// 현재 회사명 조회 (SUPER_ADMIN 전용)
useEffect(() => {
const fetchCurrentCompanyName = async () => {
if ((user as ExtendedUserInfo)?.userType === "SUPER_ADMIN") {
const companyCode = (user as ExtendedUserInfo)?.companyCode;
if (companyCode === "*") {
setCurrentCompanyName("WACE (최고 관리자)");
} else if (companyCode) {
try {
const response = await apiClient.get("/admin/companies/db");
if (response.data.success) {
const company = response.data.data.find((c: any) => c.company_code === companyCode);
setCurrentCompanyName(company?.company_name || companyCode);
}
} catch (error) {
setCurrentCompanyName(companyCode);
}
}
}
};
fetchCurrentCompanyName();
}, [(user as ExtendedUserInfo)?.companyCode, (user as ExtendedUserInfo)?.userType]);
// 화면 크기 감지 및 사이드바 초기 상태 설정
useEffect(() => {
const checkIsMobile = () => {
const mobile = window.innerWidth < 1024; // lg 브레이크포인트
setIsMobile(mobile);
// 모바일에서만 사이드바를 닫음
if (mobile) {
setSidebarOpen(false);
} else {
setSidebarOpen(true);
}
};
checkIsMobile();
window.addEventListener("resize", checkIsMobile);
return () => window.removeEventListener("resize", checkIsMobile);
}, []);
2025-08-26 17:20:45 +09:00
// 프로필 관련 로직
const {
isModalOpen,
formData,
selectedImage,
isSaving,
2025-08-28 10:05:06 +09:00
departments,
2025-08-26 17:20:45 +09:00
alertModal,
closeAlert,
openProfileModal,
closeProfileModal,
updateFormData,
selectImage,
removeImage,
saveProfile,
2025-12-01 18:41:02 +09:00
// 운전자 관련
isDriver,
hasVehicle,
2025-12-01 18:41:02 +09:00
driverInfo,
driverFormData,
updateDriverFormData,
handleDriverStatusChange,
handleDriverAccountDelete,
handleDeleteVehicle,
openVehicleRegisterModal,
closeVehicleRegisterModal,
isVehicleRegisterModalOpen,
newVehicleData,
updateNewVehicleData,
handleRegisterVehicle,
} = useProfile(user, refreshUserData, refreshMenus);
2025-08-26 17:20:45 +09:00
2025-09-04 17:01:07 +09:00
// 현재 경로에 따라 어드민 모드인지 판단 (쿼리 파라미터도 고려)
const isAdminMode = pathname.startsWith("/admin") || searchParams.get("mode") === "admin";
2025-08-26 17:20:45 +09:00
// 현재 모드에 따라 표시할 메뉴 결정
2025-10-13 19:18:01 +09:00
// 관리자 모드에서는 관리자 메뉴만, 사용자 모드에서는 사용자 메뉴만 표시
const currentMenus = isAdminMode ? adminMenus : userMenus;
2025-08-26 17:20:45 +09:00
// 메뉴 토글 함수
const toggleMenu = (menuId: string) => {
const newExpanded = new Set(expandedMenus);
if (newExpanded.has(menuId)) {
newExpanded.delete(menuId);
} else {
newExpanded.add(menuId);
}
setExpandedMenus(newExpanded);
};
// 메뉴 클릭 핸들러
2025-09-01 18:42:59 +09:00
const handleMenuClick = async (menu: any) => {
2025-08-26 17:20:45 +09:00
if (menu.hasChildren) {
toggleMenu(menu.id);
2025-09-01 18:42:59 +09:00
} else {
// 메뉴 이름 저장 (엑셀 다운로드 파일명에 사용)
const menuName = menu.label || menu.name || "메뉴";
if (typeof window !== "undefined") {
localStorage.setItem("currentMenuName", menuName);
}
2025-09-01 18:42:59 +09:00
// 먼저 할당된 화면이 있는지 확인 (URL 유무와 관계없이)
try {
const menuObjid = menu.objid || menu.id;
const assignedScreens = await menuScreenApi.getScreensByMenu(menuObjid);
if (assignedScreens.length > 0) {
// 할당된 화면이 있으면 첫 번째 화면으로 이동
const firstScreen = assignedScreens[0];
2025-09-04 17:01:07 +09:00
// 관리자 모드 상태와 menuObjid를 쿼리 파라미터로 전달
const params = new URLSearchParams();
if (isAdminMode) {
params.set("mode", "admin");
}
params.set("menuObjid", menuObjid.toString());
const screenPath = `/screens/${firstScreen.screenId}?${params.toString()}`;
2025-09-04 17:01:07 +09:00
router.push(screenPath);
if (isMobile) {
setSidebarOpen(false);
}
2025-09-01 18:42:59 +09:00
return;
}
} catch (error) {
console.warn("할당된 화면 조회 실패:", error);
}
// 할당된 화면이 없고 URL이 있으면 기존 URL로 이동
if (menu.url && menu.url !== "#") {
router.push(menu.url);
if (isMobile) {
setSidebarOpen(false);
}
2025-09-01 18:42:59 +09:00
} else {
// URL도 없고 할당된 화면도 없으면 경고 메시지
toast.warning("이 메뉴에는 연결된 페이지나 화면이 없습니다.");
}
2025-08-26 17:20:45 +09:00
}
};
// 모드 전환 핸들러
const handleModeSwitch = async () => {
2025-08-26 17:20:45 +09:00
if (isAdminMode) {
// 관리자 → 사용자 모드: 선택한 회사 유지
2025-08-26 17:20:45 +09:00
router.push("/main");
} else {
// 사용자 → 관리자 모드: WACE로 복귀 필요 (SUPER_ADMIN만)
if ((user as ExtendedUserInfo)?.userType === "SUPER_ADMIN") {
const currentCompanyCode = (user as ExtendedUserInfo)?.companyCode;
// 이미 WACE("*")가 아니면 WACE로 전환 후 관리자 페이지로 이동
if (currentCompanyCode !== "*") {
const result = await switchCompany("*");
if (result.success) {
// 페이지 새로고침 (관리자 페이지로 이동)
window.location.href = "/admin";
} else {
toast.error("WACE로 전환 실패");
}
} else {
// 이미 WACE면 바로 관리자 페이지로 이동
router.push("/admin");
}
} else {
// 일반 관리자는 바로 관리자 페이지로 이동
router.push("/admin");
}
2025-08-26 17:20:45 +09:00
}
};
// 로그아웃 핸들러
const handleLogout = async () => {
try {
await logout();
router.push("/login");
} catch (error) {
// 로그아웃 실패 시 처리
2025-08-26 17:20:45 +09:00
}
};
// 메뉴 트리 렌더링 (기존 MainLayout 스타일 적용)
const renderMenu = (menu: any, level: number = 0) => {
const isExpanded = expandedMenus.has(menu.id);
return (
<div key={menu.id}>
<div
className={`group flex h-10 cursor-pointer items-center justify-between rounded-lg px-3 py-2 text-sm font-medium transition-colors duration-200 ease-in-out ${
pathname === menu.url
2025-10-13 19:18:01 +09:00
? "border-primary border-l-4 bg-gradient-to-br from-slate-100 to-blue-100/40 text-slate-900"
: isExpanded
? "bg-slate-100 text-slate-900"
: "text-slate-600 hover:bg-slate-50 hover:text-slate-900"
2025-08-26 17:20:45 +09:00
} ${level > 0 ? "ml-6" : ""}`}
onClick={() => handleMenuClick(menu)}
>
<div className="flex min-w-0 flex-1 items-center">
2025-08-26 17:20:45 +09:00
{menu.icon}
<span className="ml-3 truncate" title={menu.name}>
{menu.name}
</span>
2025-08-26 17:20:45 +09:00
</div>
{menu.hasChildren && (
<div className="ml-auto">
{isExpanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
</div>
)}
</div>
{/* 서브메뉴 */}
{menu.hasChildren && isExpanded && (
<div className="mt-1 space-y-1 pl-6">
{menu.children?.map((child: any) => (
<div
key={child.id}
className={`flex cursor-pointer items-center rounded-lg px-3 py-2 text-sm transition-colors hover:cursor-pointer ${
pathname === child.url
2025-10-13 19:18:01 +09:00
? "border-primary border-l-4 bg-gradient-to-br from-slate-100 to-blue-100/40 text-slate-900"
: "text-slate-600 hover:bg-slate-50 hover:text-slate-900"
}`}
2025-08-26 17:20:45 +09:00
onClick={() => handleMenuClick(child)}
>
<div className="flex min-w-0 flex-1 items-center">
{child.icon}
<span className="ml-3 truncate" title={child.name}>
{child.name}
</span>
</div>
2025-08-26 17:20:45 +09:00
</div>
))}
</div>
)}
</div>
);
};
// 사용자 정보가 없으면 로딩 표시
if (!user) {
return (
<div className="flex h-screen items-center justify-center">
2025-08-26 17:51:23 +09:00
<div className="flex flex-col items-center">
2025-10-13 19:18:01 +09:00
<div className="border-primary mb-4 h-8 w-8 animate-spin rounded-full border-4 border-t-transparent"></div>
2025-08-26 17:51:23 +09:00
<p>...</p>
2025-08-26 17:20:45 +09:00
</div>
</div>
);
}
// UI 변환된 메뉴 데이터
const uiMenus = convertMenuToUI(currentMenus, user as ExtendedUserInfo);
return (
2025-12-03 19:05:10 +09:00
<div className="flex h-screen flex-col bg-white">
{/* 모바일 헤더 - 모바일에서만 표시 */}
{isMobile && (
<header className="fixed top-0 left-0 right-0 z-50 flex h-14 items-center justify-between border-b border-slate-200 bg-white px-4">
<div className="flex items-center gap-3">
{/* 햄버거 메뉴 버튼 */}
2025-12-03 10:03:24 +09:00
<SideMenu onSidebarToggle={() => setSidebarOpen(!sidebarOpen)} />
2025-12-03 19:05:10 +09:00
<Logo />
2025-12-03 10:03:24 +09:00
</div>
2025-12-03 19:05:10 +09:00
{/* 사용자 드롭다운 */}
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<button className="flex items-center gap-2 rounded-lg px-2 py-1 transition-colors hover:bg-slate-100">
<div className="relative flex h-8 w-8 shrink-0 overflow-hidden rounded-full">
{user.photo && user.photo.trim() !== "" && user.photo !== "null" ? (
<img
src={user.photo}
alt={user.userName || "User"}
className="aspect-square h-full w-full object-cover"
/>
) : (
<div className="flex h-full w-full items-center justify-center rounded-full bg-slate-200 text-sm font-semibold text-slate-700">
{user.userName?.substring(0, 1)?.toUpperCase() || "U"}
</div>
)}
</div>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end">
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm leading-none font-medium">
{user.userName || "사용자"}
</p>
<p className="text-muted-foreground text-xs leading-none">
{user.deptName || user.email || user.userId}
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={openProfileModal}>
<User className="mr-2 h-4 w-4" />
<span></span>
</DropdownMenuItem>
<DropdownMenuItem onClick={handleLogout}>
<LogOut className="mr-2 h-4 w-4" />
<span></span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</header>
)}
{/* 메인 컨테이너 - 모바일에서는 헤더 높이만큼 패딩 */}
<div className={`flex flex-1 ${isMobile ? "pt-14" : ""}`}>
{/* 모바일 사이드바 오버레이 */}
{sidebarOpen && isMobile && (
<div className="fixed inset-0 z-30 bg-black/50 lg:hidden" onClick={() => setSidebarOpen(false)} />
)}
{/* 왼쪽 사이드바 */}
<aside
className={`${
isMobile
? (sidebarOpen ? "translate-x-0" : "-translate-x-full") + " fixed top-14 left-0 z-40 h-[calc(100vh-56px)]"
: "relative z-auto h-screen translate-x-0"
} flex w-[200px] flex-col border-r border-slate-200 bg-white transition-transform duration-300`}
>
{/* 사이드바 최상단 - 로고 (데스크톱에서만 표시) */}
{!isMobile && (
<div className="flex h-14 items-center justify-between border-b border-slate-200 px-4">
<Logo />
</div>
)}
2025-08-26 17:20:45 +09:00
{/* WACE 관리자: 현재 관리 회사 표시 */}
{(user as ExtendedUserInfo)?.userType === "SUPER_ADMIN" && (
<div className="mx-3 mt-3 rounded-lg border bg-gradient-to-r from-primary/10 to-primary/5 p-3">
<div className="flex items-center gap-2">
<Building2 className="h-4 w-4 shrink-0 text-primary" />
<div className="min-w-0 flex-1">
<p className="text-[10px] text-muted-foreground"> </p>
<p className="truncate text-sm font-semibold" title={currentCompanyName || "로딩 중..."}>
{currentCompanyName || "로딩 중..."}
</p>
</div>
</div>
</div>
)}
2025-12-03 10:03:24 +09:00
{/* Admin/User 모드 전환 버튼 (관리자만) */}
{((user as ExtendedUserInfo)?.userType === "SUPER_ADMIN" ||
(user as ExtendedUserInfo)?.userType === "COMPANY_ADMIN" ||
(user as ExtendedUserInfo)?.userType === "admin") && (
<div className="space-y-2 border-b border-slate-200 p-3">
{/* 관리자/사용자 메뉴 전환 */}
2025-12-03 10:03:24 +09:00
<Button
onClick={handleModeSwitch}
className={`flex w-full items-center justify-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors duration-200 hover:cursor-pointer ${
isAdminMode
? "border border-orange-200 bg-orange-50 text-orange-700 hover:bg-orange-100"
: "border-primary/20 bg-accent hover:bg-primary/20 border text-blue-700"
}`}
>
{isAdminMode ? (
<>
<UserCheck className="h-4 w-4" />
</>
2025-08-26 17:20:45 +09:00
) : (
2025-12-03 10:03:24 +09:00
<>
<Shield className="h-4 w-4" />
</>
2025-08-26 17:20:45 +09:00
)}
2025-12-03 10:03:24 +09:00
</Button>
{/* WACE 관리자 전용: 회사 선택 버튼 */}
{(user as ExtendedUserInfo)?.userType === "SUPER_ADMIN" && (
<Button
onClick={() => { console.log("🔴 회사 선택 버튼 클릭!"); setShowCompanySwitcher(true); }}
className="flex w-full items-center justify-center gap-2 rounded-lg border border-purple-200 bg-purple-50 px-3 py-2 text-sm font-medium text-purple-700 transition-colors duration-200 hover:cursor-pointer hover:bg-purple-100"
>
<Building2 className="h-4 w-4" />
</Button>
)}
2025-08-26 17:20:45 +09:00
</div>
)}
2025-12-03 10:03:24 +09:00
{/* 메뉴 영역 */}
<div className="flex-1 overflow-y-auto py-4">
<nav className="space-y-1 px-3">
{loading ? (
<div className="animate-pulse space-y-2">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-8 rounded bg-slate-200"></div>
))}
</div>
) : (
uiMenus.map((menu) => renderMenu(menu))
)}
</nav>
</div>
2025-08-26 17:20:45 +09:00
2025-12-03 10:03:24 +09:00
{/* 사이드바 하단 - 사용자 프로필 */}
<div className="border-t border-slate-200 p-3">
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<button className="flex w-full items-center gap-3 rounded-lg px-2 py-2 text-left transition-colors hover:bg-slate-100">
{/* 프로필 아바타 */}
<div className="relative flex h-9 w-9 shrink-0 overflow-hidden rounded-full">
{user.photo && user.photo.trim() !== "" && user.photo !== "null" ? (
<img
src={user.photo}
alt={user.userName || "User"}
className="aspect-square h-full w-full object-cover"
/>
) : (
<div className="flex h-full w-full items-center justify-center rounded-full bg-slate-200 text-sm font-semibold text-slate-700">
{user.userName?.substring(0, 1)?.toUpperCase() || "U"}
</div>
)}
2025-08-26 17:20:45 +09:00
</div>
2025-12-03 10:03:24 +09:00
{/* 사용자 정보 */}
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-slate-900">
{user.userName || "사용자"}
</p>
<p className="truncate text-xs text-slate-500">
{user.deptName || user.email || user.userId}
</p>
</div>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="start" side="top">
<DropdownMenuLabel className="font-normal">
<div className="flex items-center space-x-3">
{/* 프로필 사진 표시 */}
<div className="relative flex h-12 w-12 shrink-0 overflow-hidden rounded-full">
{user.photo && user.photo.trim() !== "" && user.photo !== "null" ? (
<img
src={user.photo}
alt={user.userName || "User"}
className="aspect-square h-full w-full object-cover"
/>
) : (
<div className="flex h-full w-full items-center justify-center rounded-full bg-slate-200 text-base font-semibold text-slate-700">
{user.userName?.substring(0, 1)?.toUpperCase() || "U"}
</div>
)}
</div>
{/* 사용자 정보 */}
<div className="flex flex-col space-y-1">
<p className="text-sm leading-none font-medium">
{user.userName || "사용자"} ({user.userId || ""})
</p>
<p className="text-muted-foreground text-xs leading-none font-semibold">{user.email || ""}</p>
<p className="text-muted-foreground text-xs leading-none font-semibold">
{user.deptName && user.positionName
? `${user.deptName}, ${user.positionName}`
: user.deptName || user.positionName || "부서 정보 없음"}
</p>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={openProfileModal}>
<User className="mr-2 h-4 w-4" />
<span></span>
</DropdownMenuItem>
<DropdownMenuItem onClick={handleLogout}>
<LogOut className="mr-2 h-4 w-4" />
<span></span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</aside>
2025-08-26 17:20:45 +09:00
2025-12-03 19:05:10 +09:00
{/* 가운데 컨텐츠 영역 - 스크롤 가능 */}
<main className={`min-w-0 flex-1 overflow-auto bg-white ${isMobile ? "h-[calc(100vh-56px)]" : "h-screen"}`}>
{children}
</main>
</div>
2025-08-26 17:20:45 +09:00
{/* 프로필 수정 모달 */}
<ProfileModal
isOpen={isModalOpen}
user={user}
formData={formData}
selectedImage={selectedImage || ""}
2025-08-26 17:20:45 +09:00
isSaving={isSaving}
2025-08-28 10:05:06 +09:00
departments={departments}
2025-08-26 17:20:45 +09:00
alertModal={alertModal}
2025-12-01 18:41:02 +09:00
isDriver={isDriver}
hasVehicle={hasVehicle}
2025-12-01 18:41:02 +09:00
driverInfo={driverInfo}
driverFormData={driverFormData}
onDriverFormChange={updateDriverFormData}
onDriverStatusChange={handleDriverStatusChange}
onDriverAccountDelete={handleDriverAccountDelete}
onDeleteVehicle={handleDeleteVehicle}
onOpenVehicleRegisterModal={openVehicleRegisterModal}
isVehicleRegisterModalOpen={isVehicleRegisterModalOpen}
newVehicleData={newVehicleData}
onCloseVehicleRegisterModal={closeVehicleRegisterModal}
onNewVehicleDataChange={updateNewVehicleData}
onRegisterVehicle={handleRegisterVehicle}
2025-08-26 17:20:45 +09:00
onClose={closeProfileModal}
onFormChange={updateFormData}
onImageSelect={selectImage}
onImageRemove={removeImage}
onSave={saveProfile}
onAlertClose={closeAlert}
/>
{/* 회사 전환 모달 (WACE 관리자 전용) */}
<Dialog open={showCompanySwitcher} onOpenChange={setShowCompanySwitcher}>
<DialogContent className="max-w-[95vw] sm:max-w-[600px]">
<DialogHeader>
<DialogTitle className="text-base sm:text-lg"> </DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
.
</DialogDescription>
</DialogHeader>
<div className="mt-4">
<CompanySwitcher onClose={() => setShowCompanySwitcher(false)} isOpen={showCompanySwitcher} />
</div>
</DialogContent>
</Dialog>
2025-08-26 17:20:45 +09:00
</div>
);
}
2025-09-05 14:00:45 +09:00
export function AppLayout({ children }: AppLayoutProps) {
return (
<Suspense
fallback={
<div className="flex h-screen items-center justify-center">
<div className="flex flex-col items-center">
2025-10-13 19:18:01 +09:00
<div className="border-primary mb-4 h-8 w-8 animate-spin rounded-full border-4 border-t-transparent"></div>
2025-09-05 14:00:45 +09:00
<p>...</p>
</div>
</div>
}
>
<AppLayoutInner>{children}</AppLayoutInner>
</Suspense>
);
}