623 lines
22 KiB
TypeScript
623 lines
22 KiB
TypeScript
"use client";
|
|
|
|
import { useState, Suspense, useEffect } from "react";
|
|
import { useRouter, usePathname, useSearchParams } from "next/navigation";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Shield,
|
|
Menu,
|
|
Home,
|
|
Settings,
|
|
BarChart3,
|
|
FileText,
|
|
Users,
|
|
Package,
|
|
ChevronDown,
|
|
ChevronRight,
|
|
UserCheck,
|
|
LogOut,
|
|
User,
|
|
} from "lucide-react";
|
|
import { useMenu } from "@/contexts/MenuContext";
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
import { useProfile } from "@/hooks/useProfile";
|
|
import { MenuItem } from "@/lib/api/menu";
|
|
import { menuScreenApi } from "@/lib/api/screen";
|
|
import { toast } from "sonner";
|
|
import { ProfileModal } from "./ProfileModal";
|
|
import { Logo } from "./Logo";
|
|
import { SideMenu } from "./SideMenu";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
|
|
// 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" />;
|
|
if (name.includes("화면관리") || name.includes("screen")) return <FileText className="h-4 w-4" />;
|
|
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,
|
|
};
|
|
};
|
|
|
|
function AppLayoutInner({ children }: AppLayoutProps) {
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
const searchParams = useSearchParams();
|
|
const { user, logout, refreshUserData } = useAuth();
|
|
const { userMenus, adminMenus, loading, refreshMenus } = useMenu();
|
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
|
const [expandedMenus, setExpandedMenus] = useState<Set<string>>(new Set());
|
|
const [isMobile, setIsMobile] = useState(false);
|
|
|
|
// 화면 크기 감지 및 사이드바 초기 상태 설정
|
|
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);
|
|
}, []);
|
|
|
|
// 프로필 관련 로직
|
|
const {
|
|
isModalOpen,
|
|
formData,
|
|
selectedImage,
|
|
isSaving,
|
|
departments,
|
|
alertModal,
|
|
closeAlert,
|
|
openProfileModal,
|
|
closeProfileModal,
|
|
updateFormData,
|
|
selectImage,
|
|
removeImage,
|
|
saveProfile,
|
|
// 운전자 관련
|
|
isDriver,
|
|
hasVehicle,
|
|
driverInfo,
|
|
driverFormData,
|
|
updateDriverFormData,
|
|
handleDriverStatusChange,
|
|
handleDriverAccountDelete,
|
|
handleDeleteVehicle,
|
|
openVehicleRegisterModal,
|
|
closeVehicleRegisterModal,
|
|
isVehicleRegisterModalOpen,
|
|
newVehicleData,
|
|
updateNewVehicleData,
|
|
handleRegisterVehicle,
|
|
} = useProfile(user, refreshUserData, refreshMenus);
|
|
|
|
// 현재 경로에 따라 어드민 모드인지 판단 (쿼리 파라미터도 고려)
|
|
const isAdminMode = pathname.startsWith("/admin") || searchParams.get("mode") === "admin";
|
|
|
|
// 현재 모드에 따라 표시할 메뉴 결정
|
|
// 관리자 모드에서는 관리자 메뉴만, 사용자 모드에서는 사용자 메뉴만 표시
|
|
const currentMenus = isAdminMode ? adminMenus : userMenus;
|
|
|
|
// 메뉴 토글 함수
|
|
const toggleMenu = (menuId: string) => {
|
|
const newExpanded = new Set(expandedMenus);
|
|
if (newExpanded.has(menuId)) {
|
|
newExpanded.delete(menuId);
|
|
} else {
|
|
newExpanded.add(menuId);
|
|
}
|
|
setExpandedMenus(newExpanded);
|
|
};
|
|
|
|
// 메뉴 클릭 핸들러
|
|
const handleMenuClick = async (menu: any) => {
|
|
if (menu.hasChildren) {
|
|
toggleMenu(menu.id);
|
|
} else {
|
|
// 메뉴 이름 저장 (엑셀 다운로드 파일명에 사용)
|
|
const menuName = menu.label || menu.name || "메뉴";
|
|
if (typeof window !== "undefined") {
|
|
localStorage.setItem("currentMenuName", menuName);
|
|
}
|
|
|
|
// 먼저 할당된 화면이 있는지 확인 (URL 유무와 관계없이)
|
|
try {
|
|
const menuObjid = menu.objid || menu.id;
|
|
const assignedScreens = await menuScreenApi.getScreensByMenu(menuObjid);
|
|
|
|
if (assignedScreens.length > 0) {
|
|
// 할당된 화면이 있으면 첫 번째 화면으로 이동
|
|
const firstScreen = assignedScreens[0];
|
|
|
|
// 관리자 모드 상태와 menuObjid를 쿼리 파라미터로 전달
|
|
const params = new URLSearchParams();
|
|
if (isAdminMode) {
|
|
params.set("mode", "admin");
|
|
}
|
|
params.set("menuObjid", menuObjid.toString());
|
|
|
|
const screenPath = `/screens/${firstScreen.screenId}?${params.toString()}`;
|
|
|
|
router.push(screenPath);
|
|
if (isMobile) {
|
|
setSidebarOpen(false);
|
|
}
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
console.warn("할당된 화면 조회 실패:", error);
|
|
}
|
|
|
|
// 할당된 화면이 없고 URL이 있으면 기존 URL로 이동
|
|
if (menu.url && menu.url !== "#") {
|
|
router.push(menu.url);
|
|
if (isMobile) {
|
|
setSidebarOpen(false);
|
|
}
|
|
} else {
|
|
// URL도 없고 할당된 화면도 없으면 경고 메시지
|
|
toast.warning("이 메뉴에는 연결된 페이지나 화면이 없습니다.");
|
|
}
|
|
}
|
|
};
|
|
|
|
// 모드 전환 핸들러
|
|
const handleModeSwitch = () => {
|
|
if (isAdminMode) {
|
|
router.push("/main");
|
|
} else {
|
|
router.push("/admin");
|
|
}
|
|
};
|
|
|
|
// 로그아웃 핸들러
|
|
const handleLogout = async () => {
|
|
try {
|
|
await logout();
|
|
router.push("/login");
|
|
} catch (error) {
|
|
// 로그아웃 실패 시 처리
|
|
}
|
|
};
|
|
|
|
// 메뉴 트리 렌더링 (기존 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
|
|
? "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"
|
|
} ${level > 0 ? "ml-6" : ""}`}
|
|
onClick={() => handleMenuClick(menu)}
|
|
>
|
|
<div className="flex min-w-0 flex-1 items-center">
|
|
{menu.icon}
|
|
<span className="ml-3 truncate" title={menu.name}>
|
|
{menu.name}
|
|
</span>
|
|
</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
|
|
? "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"
|
|
}`}
|
|
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>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// 사용자 정보가 없으면 로딩 표시
|
|
if (!user) {
|
|
return (
|
|
<div className="flex h-screen items-center justify-center">
|
|
<div className="flex flex-col items-center">
|
|
<div className="border-primary mb-4 h-8 w-8 animate-spin rounded-full border-4 border-t-transparent"></div>
|
|
<p>로딩중...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// UI 변환된 메뉴 데이터
|
|
const uiMenus = convertMenuToUI(currentMenus, user as ExtendedUserInfo);
|
|
|
|
return (
|
|
<div className="flex h-screen bg-white">
|
|
{/* 모바일 사이드바 오버레이 */}
|
|
{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-0 left-0 z-40"
|
|
: "relative z-auto translate-x-0"
|
|
} flex h-screen w-[200px] flex-col border-r border-slate-200 bg-white transition-transform duration-300`}
|
|
>
|
|
{/* 사이드바 최상단 - 로고 + 모바일 햄버거 메뉴 */}
|
|
<div className="flex h-14 items-center justify-between border-b border-slate-200 px-4">
|
|
<Logo />
|
|
{/* 모바일 햄버거 메뉴 버튼 */}
|
|
<div className="lg:hidden">
|
|
<SideMenu onSidebarToggle={() => setSidebarOpen(!sidebarOpen)} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Admin/User 모드 전환 버튼 (관리자만) */}
|
|
{((user as ExtendedUserInfo)?.userType === "SUPER_ADMIN" ||
|
|
(user as ExtendedUserInfo)?.userType === "COMPANY_ADMIN" ||
|
|
(user as ExtendedUserInfo)?.userType === "admin") && (
|
|
<div className="border-b border-slate-200 p-3">
|
|
<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" />
|
|
사용자 메뉴로 전환
|
|
</>
|
|
) : (
|
|
<>
|
|
<Shield className="h-4 w-4" />
|
|
관리자 메뉴로 전환
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{/* 메뉴 영역 */}
|
|
<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>
|
|
|
|
{/* 사이드바 하단 - 사용자 프로필 */}
|
|
<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>
|
|
)}
|
|
</div>
|
|
{/* 사용자 정보 */}
|
|
<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>
|
|
|
|
{/* 가운데 컨텐츠 영역 - 스크롤 가능 */}
|
|
<main className="h-screen min-w-0 flex-1 overflow-auto bg-white">
|
|
{children}
|
|
</main>
|
|
|
|
{/* 프로필 수정 모달 */}
|
|
<ProfileModal
|
|
isOpen={isModalOpen}
|
|
user={user}
|
|
formData={formData}
|
|
selectedImage={selectedImage || ""}
|
|
isSaving={isSaving}
|
|
departments={departments}
|
|
alertModal={alertModal}
|
|
isDriver={isDriver}
|
|
hasVehicle={hasVehicle}
|
|
driverInfo={driverInfo}
|
|
driverFormData={driverFormData}
|
|
onDriverFormChange={updateDriverFormData}
|
|
onDriverStatusChange={handleDriverStatusChange}
|
|
onDriverAccountDelete={handleDriverAccountDelete}
|
|
onDeleteVehicle={handleDeleteVehicle}
|
|
onOpenVehicleRegisterModal={openVehicleRegisterModal}
|
|
isVehicleRegisterModalOpen={isVehicleRegisterModalOpen}
|
|
newVehicleData={newVehicleData}
|
|
onCloseVehicleRegisterModal={closeVehicleRegisterModal}
|
|
onNewVehicleDataChange={updateNewVehicleData}
|
|
onRegisterVehicle={handleRegisterVehicle}
|
|
onClose={closeProfileModal}
|
|
onFormChange={updateFormData}
|
|
onImageSelect={selectImage}
|
|
onImageRemove={removeImage}
|
|
onSave={saveProfile}
|
|
onAlertClose={closeAlert}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function AppLayout({ children }: AppLayoutProps) {
|
|
return (
|
|
<Suspense
|
|
fallback={
|
|
<div className="flex h-screen items-center justify-center">
|
|
<div className="flex flex-col items-center">
|
|
<div className="border-primary mb-4 h-8 w-8 animate-spin rounded-full border-4 border-t-transparent"></div>
|
|
<p>로딩중...</p>
|
|
</div>
|
|
</div>
|
|
}
|
|
>
|
|
<AppLayoutInner>{children}</AppLayoutInner>
|
|
</Suspense>
|
|
);
|
|
}
|