Merge branch 'gbpark-node' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node

This commit is contained in:
kjs
2026-03-11 12:23:52 +09:00
588 changed files with 25062 additions and 13798 deletions
+152 -214
View File
@@ -33,6 +33,7 @@ import { SideMenu } from "./SideMenu";
import { TabBar } from "./TabBar";
import { TabContent } from "./TabContent";
import { useTabStore } from "@/stores/tabStore";
import { ThemeToggle } from "./ThemeToggle";
import {
DropdownMenu,
DropdownMenuContent,
@@ -51,7 +52,6 @@ import {
import { CompanySwitcher } from "@/components/admin/CompanySwitcher";
import { getIconComponent } from "@/components/admin/MenuIconPicker";
// useAuth의 UserInfo 타입을 확장
interface ExtendedUserInfo {
userId: string;
userName: string;
@@ -79,7 +79,6 @@ interface AppLayoutProps {
children: React.ReactNode;
}
// 메뉴 아이콘 매핑 함수 (DB 아이콘 우선, 없으면 키워드 기반 fallback)
const getMenuIcon = (menuName: string, dbIconName?: string | null) => {
if (dbIconName) {
const DbIcon = getIconComponent(dbIconName);
@@ -99,15 +98,12 @@ const getMenuIcon = (menuName: string, dbIconName?: string | null) => {
return <FileText className="h-4 w-4" />;
};
// 메뉴 데이터를 UI용으로 변환하는 함수 (최상위 "사용자", "관리자" 제외)
// parentPath: 탭 제목에 "기준정보 - 회사관리" 형태로 상위 카테고리를 포함하기 위한 경로
const convertMenuToUI = (menus: MenuItem[], userInfo: ExtendedUserInfo | null, parentId: string = "0", parentPath: string = ""): 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[] = [];
@@ -128,11 +124,9 @@ const convertMenuToUI = (menus: MenuItem[], userInfo: ExtendedUserInfo | null, p
return filteredMenus.map((menu) => convertSingleMenu(menu, menus, userInfo, parentPath));
};
// 단일 메뉴 변환 함수
const convertSingleMenu = (menu: MenuItem, allMenus: MenuItem[], userInfo: ExtendedUserInfo | null, parentPath: string = ""): any => {
const menuId = menu.objid || menu.OBJID;
// 사용자 locale 기준으로 번역 처리
const getDisplayText = (m: MenuItem) => {
if (m.translated_name || m.TRANSLATED_NAME) {
return m.translated_name || m.TRANSLATED_NAME;
@@ -223,7 +217,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const { user, logout, refreshUserData, switchCompany } = useAuth();
const { user, logout, refreshUserData } = useAuth();
const { userMenus, adminMenus, loading, refreshMenus } = useMenu();
const [sidebarOpen, setSidebarOpen] = useState(true);
const [expandedMenus, setExpandedMenus] = useState<Set<string>>(new Set());
@@ -231,13 +225,12 @@ function AppLayoutInner({ children }: AppLayoutProps) {
const [showCompanySwitcher, setShowCompanySwitcher] = useState(false);
const [currentCompanyName, setCurrentCompanyName] = useState<string>("");
// URL 직접 접근 시 탭 자동 열기 (북마크/공유 링크 대응)
// URL 직접 접근 시 탭 자동 열기
useEffect(() => {
const store = useTabStore.getState();
const currentModeTabs = store[store.mode].tabs;
if (currentModeTabs.length > 0) return;
// /screens/[screenId] 패턴 감지
const screenMatch = pathname.match(/^\/screens\/(\d+)/);
if (screenMatch) {
const screenId = parseInt(screenMatch[1]);
@@ -246,19 +239,18 @@ function AppLayoutInner({ children }: AppLayoutProps) {
return;
}
// /admin/* 패턴 감지 -> admin 모드로 전환 후 탭 열기
if (pathname.startsWith("/admin") && pathname !== "/admin") {
store.setMode("admin");
store.openTab({ type: "admin", title: pathname.split("/").pop() || "관리자", adminUrl: pathname });
}
}, []); // 마운트 시 1회만 실행
}, []);
// 현재 회사명 조회 (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) {
@@ -268,7 +260,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
const company = response.data.data.find((c: any) => c.company_code === companyCode);
setCurrentCompanyName(company?.company_name || companyCode);
}
} catch (error) {
} catch {
setCurrentCompanyName(companyCode);
}
}
@@ -281,9 +273,8 @@ function AppLayoutInner({ children }: AppLayoutProps) {
// 화면 크기 감지 및 사이드바 초기 상태 설정
useEffect(() => {
const checkIsMobile = () => {
const mobile = window.innerWidth < 1024; // lg 브레이크포인트
const mobile = window.innerWidth < 1024;
setIsMobile(mobile);
// 모바일에서만 사이드바를 닫음
if (mobile) {
setSidebarOpen(false);
} else {
@@ -311,7 +302,6 @@ function AppLayoutInner({ children }: AppLayoutProps) {
selectImage,
removeImage,
saveProfile,
// 운전자 관련
isDriver,
hasVehicle,
driverInfo,
@@ -328,19 +318,14 @@ function AppLayoutInner({ children }: AppLayoutProps) {
handleRegisterVehicle,
} = useProfile(user, refreshUserData, refreshMenus);
// 탭 스토어에서 현재 모드 가져오기
const tabMode = useTabStore((s) => s.mode);
const setTabMode = useTabStore((s) => s.setMode);
const isAdminMode = tabMode === "admin";
// 프리뷰 모드: 사이드바/헤더 없이 화면만 표시 (iframe 임베드용)
const isPreviewMode = searchParams.get("preview") === "true";
// 현재 모드에 따라 표시할 메뉴 결정
// 관리자 모드에서는 관리자 메뉴만, 사용자 모드에서는 사용자 메뉴만 표시
const currentMenus = isAdminMode ? adminMenus : userMenus;
// 메뉴 토글 함수
const toggleMenu = (menuId: string) => {
const newExpanded = new Set(expandedMenus);
if (newExpanded.has(menuId)) {
@@ -353,12 +338,10 @@ function AppLayoutInner({ children }: AppLayoutProps) {
const { openTab } = useTabStore();
// 메뉴 클릭 핸들러 (탭으로 열기)
const handleMenuClick = async (menu: any) => {
if (menu.hasChildren) {
toggleMenu(menu.id);
} else {
// tabTitle: "기준정보 - 회사관리" 형태의 상위 포함 이름
const menuName = menu.tabTitle || menu.label || menu.name || "메뉴";
if (typeof window !== "undefined") {
localStorage.setItem("currentMenuName", menuName);
@@ -379,8 +362,8 @@ function AppLayoutInner({ children }: AppLayoutProps) {
if (isMobile) setSidebarOpen(false);
return;
}
} catch (error) {
console.warn("할당된 화면 조회 실패:", error);
} catch {
console.warn("할당된 화면 조회 실패");
}
if (menu.url && menu.url !== "#") {
@@ -396,50 +379,19 @@ function AppLayoutInner({ children }: AppLayoutProps) {
}
};
// 모드 전환 핸들러
// 모드 전환: 탭 스토어의 모드만 변경 (각 모드 탭은 독립 보존)
const handleModeSwitch = () => {
setTabMode(isAdminMode ? "user" : "admin");
};
// 로그아웃 핸들러
const handleLogout = async () => {
try {
await logout();
router.push("/login");
} catch (error) {
// 로그아웃 실패 시 처리
} catch {
// 로그아웃 실패
}
};
// 사이드바 메뉴 -> 탭 바 드래그용 데이터 생성
const buildMenuDragData = async (menu: any): Promise<string | null> => {
const menuName = menu.label || menu.name || "메뉴";
const menuObjid = menu.objid || menu.id;
try {
const assignedScreens = await menuScreenApi.getScreensByMenu(menuObjid);
if (assignedScreens.length > 0) {
return JSON.stringify({
type: "screen" as const,
title: menuName,
screenId: assignedScreens[0].screenId,
menuObjid: parseInt(menuObjid),
});
}
} catch { /* ignore */ }
if (menu.url && menu.url !== "#") {
return JSON.stringify({
type: "admin" as const,
title: menuName,
adminUrl: menu.url,
});
}
return null;
};
const handleMenuDragStart = (e: React.DragEvent, menu: any) => {
if (menu.hasChildren) {
e.preventDefault();
@@ -453,7 +405,6 @@ function AppLayoutInner({ children }: AppLayoutProps) {
e.dataTransfer.setData("text/plain", menuName);
};
// 메뉴 트리 렌더링 (드래그 가능)
const renderMenu = (menu: any, level: number = 0) => {
const isExpanded = expandedMenus.has(menu.id);
const isLeaf = !menu.hasChildren;
@@ -463,12 +414,12 @@ function AppLayoutInner({ children }: AppLayoutProps) {
<div
draggable={isLeaf}
onDragStart={(e) => handleMenuDragStart(e, menu)}
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 ${
className={`group flex min-h-[44px] cursor-pointer items-center justify-between rounded-md px-3 py-2 text-sm font-medium transition-colors duration-150 ease-in-out sm:min-h-[40px] ${
pathname === menu.url
? "border-primary border-l-4 bg-gradient-to-br from-slate-100 to-blue-100/40 text-slate-900"
? "border-primary bg-primary/8 text-primary border-l-3 font-semibold"
: isExpanded
? "bg-slate-100 text-slate-900"
: "text-slate-600 hover:bg-slate-50 hover:text-slate-900"
? "bg-accent/60 text-foreground"
: "text-muted-foreground hover:bg-accent hover:text-foreground"
} ${level > 0 ? "ml-6" : ""}`}
onClick={() => handleMenuClick(menu)}
>
@@ -485,18 +436,17 @@ function AppLayoutInner({ children }: AppLayoutProps) {
)}
</div>
{/* 서브메뉴 */}
{menu.hasChildren && isExpanded && (
<div className="mt-1 space-y-1 pl-6">
<div className="mt-0.5 space-y-0.5 pl-9">
{menu.children?.map((child: any) => (
<div
key={child.id}
draggable={!child.hasChildren}
onDragStart={(e) => handleMenuDragStart(e, child)}
className={`flex cursor-pointer items-center rounded-lg px-3 py-2 text-sm transition-colors hover:cursor-pointer ${
className={`flex min-h-[44px] cursor-pointer items-center rounded-md px-3 py-2 text-sm transition-colors duration-150 hover:cursor-pointer sm:min-h-[40px] ${
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"
? "border-primary bg-primary/8 text-primary border-l-3 font-semibold"
: "text-muted-foreground hover:bg-accent hover:text-foreground"
}`}
onClick={() => handleMenuClick(child)}
>
@@ -514,16 +464,12 @@ function AppLayoutInner({ children }: AppLayoutProps) {
);
};
// 프리뷰 모드: 사이드바/헤더 없이 화면만 표시 (인증 대기 없이 즉시 렌더링)
if (isPreviewMode) {
return (
<div className="h-screen w-full overflow-auto bg-white p-4">
{children}
</div>
<div className="bg-background h-screen w-full overflow-auto p-4">{children}</div>
);
}
// 사용자 정보가 없으면 로딩 표시
if (!user) {
return (
<div className="flex h-screen items-center justify-center">
@@ -535,23 +481,20 @@ function AppLayoutInner({ children }: AppLayoutProps) {
);
}
// UI 변환된 메뉴 데이터
const uiMenus = convertMenuToUI(currentMenus, user as ExtendedUserInfo);
return (
<div className="flex h-screen flex-col bg-white">
{/* 모바일 헤더 - 모바일에서만 표시 */}
<div className="bg-background flex h-screen flex-col">
{/* 모바일 헤더 */}
{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">
<header className="border-border bg-background/95 fixed top-0 right-0 left-0 z-50 flex h-14 items-center justify-between border-b px-4 backdrop-blur-sm">
<div className="flex items-center gap-3">
{/* 햄버거 메뉴 버튼 */}
<SideMenu onSidebarToggle={() => setSidebarOpen(!sidebarOpen)} />
<Logo />
</div>
{/* 사용자 드롭다운 */}
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<button className="flex items-center gap-2 rounded-lg px-2 py-1 transition-colors hover:bg-slate-100">
<button className="hover:bg-accent flex items-center gap-2 rounded-lg px-2 py-1 transition-colors">
<div className="relative flex h-8 w-8 shrink-0 overflow-hidden rounded-full">
{user.photo && user.photo.trim() !== "" && user.photo !== "null" ? (
<img
@@ -560,7 +503,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
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">
<div className="bg-muted text-foreground flex h-full w-full items-center justify-center rounded-full text-sm font-semibold">
{user.userName?.substring(0, 1)?.toUpperCase() || "U"}
</div>
)}
@@ -570,9 +513,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
<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-sm leading-none font-medium">{user.userName || "사용자"}</p>
<p className="text-muted-foreground text-xs leading-none">
{user.deptName || user.email || user.userId}
</p>
@@ -588,6 +529,10 @@ function AppLayoutInner({ children }: AppLayoutProps) {
<span></span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<div className="px-1 py-0.5">
<ThemeToggle />
</div>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout}>
<LogOut className="mr-2 h-4 w-4" />
<span></span>
@@ -597,11 +542,13 @@ function AppLayoutInner({ children }: AppLayoutProps) {
</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)} />
<div
className="fixed inset-0 z-30 bg-black/40 backdrop-blur-sm lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
{/* 왼쪽 사이드바 */}
@@ -610,22 +557,20 @@ function AppLayoutInner({ children }: AppLayoutProps) {
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`}
} border-sidebar-border bg-sidebar flex w-[260px] flex-col border-r transition-transform duration-300 sm:w-[220px] lg:w-[240px]`}
>
{/* 사이드바 최상단 - 로고 (데스크톱에서만 표시) */}
{!isMobile && (
<div className="flex h-14 items-center justify-between border-b border-slate-200 px-4">
<div className="border-border flex h-14 items-center justify-between border-b px-4">
<Logo />
</div>
)}
{/* 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="border-border bg-muted/50 mx-3 mt-3 rounded-md border p-3">
<div className="flex items-center gap-2">
<Building2 className="h-4 w-4 shrink-0 text-primary" />
<Building2 className="text-primary h-4 w-4 shrink-0" />
<div className="min-w-0 flex-1">
<p className="text-[10px] text-muted-foreground"> </p>
<p className="text-muted-foreground text-[10px]"> </p>
<p className="truncate text-sm font-semibold" title={currentCompanyName || "로딩 중..."}>
{currentCompanyName || "로딩 중..."}
</p>
@@ -634,96 +579,69 @@ function AppLayoutInner({ children }: AppLayoutProps) {
</div>
)}
{/* 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">
{/* 관리자/사용자 메뉴 전환 */}
<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>
{/* WACE 관리자 전용: 회사 선택 버튼 */}
{(user as ExtendedUserInfo)?.userType === "SUPER_ADMIN" && (
{((user as ExtendedUserInfo)?.userType === "SUPER_ADMIN" ||
(user as ExtendedUserInfo)?.userType === "COMPANY_ADMIN" ||
(user as ExtendedUserInfo)?.userType === "admin") && (
<div className="border-border space-y-2 border-b p-3">
<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"
onClick={handleModeSwitch}
className={`flex w-full items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors duration-150 hover:cursor-pointer ${
isAdminMode
? "border border-amber-200 bg-amber-50 text-amber-700 hover:bg-amber-100 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-400"
: "border-primary/20 bg-primary/5 text-primary hover:bg-primary/10 border"
}`}
>
<Building2 className="h-4 w-4" />
{isAdminMode ? (
<>
<UserCheck className="h-4 w-4" />
</>
) : (
<>
<Shield className="h-4 w-4" />
</>
)}
</Button>
)}
{(user as ExtendedUserInfo)?.userType === "SUPER_ADMIN" && (
<Button
onClick={() => {
console.log("🔴 회사 선택 버튼 클릭!");
setShowCompanySwitcher(true);
}}
className="border-primary/20 bg-primary/5 text-primary hover:bg-primary/10 flex w-full items-center justify-center gap-2 rounded-md border px-3 py-2 text-sm font-medium transition-colors duration-150 hover:cursor-pointer"
>
<Building2 className="h-4 w-4" />
</Button>
)}
</div>
)}
<div className="flex-1 overflow-y-auto py-4">
<nav className="space-y-0.5 px-3">
{loading ? (
<div className="animate-pulse space-y-2">
{[...Array(5)].map((_, i) => (
<div key={i} className="bg-muted h-8 rounded"></div>
))}
</div>
) : (
uiMenus.map((menu) => renderMenu(menu))
)}
</nav>
</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-border border-t px-3 py-1">
<ThemeToggle />
</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">
<div className="border-border bg-muted/30 border-t p-3">
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<button className="hover:bg-accent flex w-full items-center gap-3 rounded-lg px-2 py-2 text-left transition-colors">
<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}
@@ -731,53 +649,74 @@ function AppLayoutInner({ children }: AppLayoutProps) {
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">
<div className="bg-muted text-foreground flex h-full w-full items-center justify-center rounded-full text-sm font-semibold">
{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 || "부서 정보 없음"}
<div className="min-w-0 flex-1">
<p className="text-foreground truncate text-sm font-medium">{user.userName || "사용자"}</p>
<p className="text-muted-foreground truncate text-xs">
{user.deptName || user.email || user.userId}
</p>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={openProfileModal}>
<User className="mr-2 h-4 w-4" />
<span></span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => router.push("/admin/approvalBox")}>
<FileCheck className="mr-2 h-4 w-4" />
<span></span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout}>
<LogOut className="mr-2 h-4 w-4" />
<span></span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</aside>
</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="bg-muted text-foreground flex h-full w-full items-center justify-center rounded-full text-base font-semibold">
{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={() => router.push("/admin/approvalBox")}>
<FileCheck className="mr-2 h-4 w-4" />
<span></span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout}>
<LogOut className="mr-2 h-4 w-4" />
<span></span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</aside>
{/* 가운데 컨텐츠 영역 - 탭 시스템 */}
<main className={`flex min-w-0 flex-1 flex-col overflow-hidden bg-white ${isMobile ? "h-[calc(100vh-56px)]" : "h-screen"}`}>
<main className={`flex min-w-0 flex-1 flex-col overflow-hidden bg-background ${isMobile ? "h-[calc(100vh-56px)]" : "h-screen"}`}>
<TabBar />
<TabContent />
</main>
</div>
{/* 프로필 수정 모달 */}
<ProfileModal
isOpen={isModalOpen}
user={user}
@@ -808,7 +747,6 @@ function AppLayoutInner({ children }: AppLayoutProps) {
onAlertClose={closeAlert}
/>
{/* 회사 전환 모달 (WACE 관리자 전용) */}
<Dialog open={showCompanySwitcher} onOpenChange={setShowCompanySwitcher}>
<DialogContent className="max-w-[95vw] sm:max-w-[600px]">
<DialogHeader>