diff --git a/fems-app/src/app/(admin)/layout.tsx b/fems-app/src/app/(admin)/layout.tsx index 51746a8..a9ac5fd 100644 --- a/fems-app/src/app/(admin)/layout.tsx +++ b/fems-app/src/app/(admin)/layout.tsx @@ -1,13 +1,10 @@ // src/app/(admin)/layout.tsx +import React from "react"; import AdminGuard from "@/components/auth/AdminGuard"; import { SideNav } from "@/components/layout/SideNav"; import { TopNav } from "@/components/layout/TopNav"; -export default function AdminLayout({ - children, -}: { - children: React.ReactNode; -}) { +const AdminLayout = ({ children }: { children: React.ReactNode }) => { return ( <AdminGuard> <div className="h-screen flex"> @@ -29,4 +26,6 @@ export default function AdminLayout({ </div> </AdminGuard> ); -} +}; + +export default AdminLayout; diff --git a/fems-app/src/app/(admin)/users/accounts/page.tsx b/fems-app/src/app/(admin)/users/accounts/page.tsx index 5780d4b..6be2046 100644 --- a/fems-app/src/app/(admin)/users/accounts/page.tsx +++ b/fems-app/src/app/(admin)/users/accounts/page.tsx @@ -22,6 +22,7 @@ import { AxiosError } from "axios"; import { UserForm } from "./components/UserForm"; import { Switch } from "@/components/ui/switch"; import { User } from "@/types/user"; +import AdminGuard from "@/components/auth/AdminGuard"; const AccountsPage = () => { const { token, user } = useAuthStore(); @@ -225,71 +226,73 @@ const AccountsPage = () => { if (isLoading) return <div>Loading...</div>; return ( - <div className="container mx-auto py-6"> - {/* Header */} - <div className="flex justify-between items-center mb-6"> - <div className="space-y-1"> - <h1 className="text-3xl font-bold">유저 관리</h1> - <p className="text-muted-foreground"> - 유저를 관리하고 권한을 설정합니다. - </p> + <AdminGuard requiredPermissions={["users:manage"]}> + <div className="container mx-auto py-6"> + {/* Header */} + <div className="flex justify-between items-center mb-6"> + <div className="space-y-1"> + <h1 className="text-3xl font-bold">유저 관리</h1> + <p className="text-muted-foreground"> + 유저를 관리하고 권한을 설정합니다. + </p> + </div> + <Button onClick={() => setIsOpen(true)}> + <Plus className="mr-2 h-4 w-4" /> + 유저 추가 + </Button> </div> - <Button onClick={() => setIsOpen(true)}> - <Plus className="mr-2 h-4 w-4" /> - 유저 추가 - </Button> + + {/* Users Table */} + <Card> + <CardHeader> + <CardTitle>유저 목록</CardTitle> + </CardHeader> + <CardContent> + {users && users.length > 0 ? ( + <DataTable columns={columns} data={users} /> + ) : ( + <div className="text-center py-12 text-muted-foreground"> + 등록된 유저가 없습니다. + </div> + )} + </CardContent> + </Card> + + {/* User Create/Edit Dialog */} + <Dialog + open={isOpen} + onOpenChange={(open) => { + setIsOpen(open); + if (!open) setEditingUser(null); + }} + > + <DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto"> + <DialogHeader> + <DialogTitle>{editingUser ? "유저 수정" : "새 유저"}</DialogTitle> + <DialogDescription> + {editingUser + ? "기존 유저 정보를 수정합니다." + : "새로운 유저를 생성합니다."} + </DialogDescription> + </DialogHeader> + <UserForm + initialData={editingUser || undefined} + onSubmit={(data) => { + if (editingUser) { + updateMutation.mutate({ id: editingUser.id, ...data }); + } else { + createMutation.mutate(data); + } + }} + onCancel={() => { + setIsOpen(false); + setEditingUser(null); + }} + /> + </DialogContent> + </Dialog> </div> - - {/* Users Table */} - <Card> - <CardHeader> - <CardTitle>유저 목록</CardTitle> - </CardHeader> - <CardContent> - {users && users.length > 0 ? ( - <DataTable columns={columns} data={users} /> - ) : ( - <div className="text-center py-12 text-muted-foreground"> - 등록된 유저가 없습니다. - </div> - )} - </CardContent> - </Card> - - {/* User Create/Edit Dialog */} - <Dialog - open={isOpen} - onOpenChange={(open) => { - setIsOpen(open); - if (!open) setEditingUser(null); - }} - > - <DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto"> - <DialogHeader> - <DialogTitle>{editingUser ? "유저 수정" : "새 유저"}</DialogTitle> - <DialogDescription> - {editingUser - ? "기존 유저 정보를 수정합니다." - : "새로운 유저를 생성합니다."} - </DialogDescription> - </DialogHeader> - <UserForm - initialData={editingUser || undefined} - onSubmit={(data) => { - if (editingUser) { - updateMutation.mutate({ id: editingUser.id, ...data }); - } else { - createMutation.mutate(data); - } - }} - onCancel={() => { - setIsOpen(false); - setEditingUser(null); - }} - /> - </DialogContent> - </Dialog> - </div> + </AdminGuard> ); }; diff --git a/fems-app/src/components/auth/AdminGuard.tsx b/fems-app/src/components/auth/AdminGuard.tsx index 7c8e655..eb16471 100644 --- a/fems-app/src/components/auth/AdminGuard.tsx +++ b/fems-app/src/components/auth/AdminGuard.tsx @@ -1,13 +1,52 @@ +// // src/components/auth/AdminGuard.tsx +// "use client"; + +// import { usePermissions } from "@/hooks/usePermissions"; +// import { ReactNode } from "react"; + +// interface AdminGuardProps { +// children: ReactNode; +// requiredPermissions?: string[]; // 필요한 권한 목록 +// requireAll?: boolean; // true면 모든 권한 필요, false면 하나라도 있으면 됨 +// } + +// export default function AdminGuard({ +// children, +// requiredPermissions = [], +// requireAll = false, +// }: AdminGuardProps) { +// const { hasAllPermissions, hasAnyPermission } = usePermissions(); + +// const hasAccess = +// requiredPermissions.length === 0 +// ? true +// : requireAll +// ? hasAllPermissions(requiredPermissions) +// : hasAnyPermission(requiredPermissions); + +// if (!hasAccess) { +// return ( +// <div className="flex items-center justify-center min-h-[200px] text-gray-500"> +// 접근 권한이 없습니다. +// </div> +// ); +// } + +// return <>{children}</>; +// } + // src/components/auth/AdminGuard.tsx "use client"; +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; import { usePermissions } from "@/hooks/usePermissions"; -import { ReactNode } from "react"; +import { useAuth } from "@/hooks/useAuth"; interface AdminGuardProps { - children: ReactNode; - requiredPermissions?: string[]; // 필요한 권한 목록 - requireAll?: boolean; // true면 모든 권한 필요, false면 하나라도 있으면 됨 + children: React.ReactNode; + requiredPermissions?: string[]; + requireAll?: boolean; } export default function AdminGuard({ @@ -15,8 +54,38 @@ export default function AdminGuard({ requiredPermissions = [], requireAll = false, }: AdminGuardProps) { + const router = useRouter(); + const { user } = useAuth(); const { hasAllPermissions, hasAnyPermission } = usePermissions(); + useEffect(() => { + // 사용자가 없거나 로그인하지 않은 경우 + if (!user) { + router.push("/login"); + return; + } + + // 권한 체크 + const hasAccess = + requiredPermissions.length === 0 + ? true + : requireAll + ? hasAllPermissions(requiredPermissions) + : hasAnyPermission(requiredPermissions); + + if (!hasAccess) { + router.push("/dashboard/overview"); + } + }, [ + user, + router, + requiredPermissions, + requireAll, + hasAllPermissions, + hasAnyPermission, + ]); + + // 권한 체크 const hasAccess = requiredPermissions.length === 0 ? true @@ -24,20 +93,13 @@ export default function AdminGuard({ ? hasAllPermissions(requiredPermissions) : hasAnyPermission(requiredPermissions); - if (!hasAccess) { + if (!user || !hasAccess) { return ( <div className="flex items-center justify-center min-h-[200px] text-gray-500"> - 접근 권한이 없습니다. + 로딩 중... </div> ); } return <>{children}</>; } - -// 사용 예시: -/* -<AdminGuard requiredPermissions={['users:manage']}> - <UserManagementPanel /> -</AdminGuard> -*/ diff --git a/fems-app/src/hooks/useAuth.ts b/fems-app/src/hooks/useAuth.ts index f071ce8..a0f4a88 100644 --- a/fems-app/src/hooks/useAuth.ts +++ b/fems-app/src/hooks/useAuth.ts @@ -37,7 +37,7 @@ export function useAuth() { const logout = () => { clearAuth(); localStorage.removeItem("token"); - router.push("/"); + router.push("/login"); }; return { user, token, login, logout }; diff --git a/fems-app/src/hooks/usePermissions.ts b/fems-app/src/hooks/usePermissions.ts index 114dab7..28f8371 100644 --- a/fems-app/src/hooks/usePermissions.ts +++ b/fems-app/src/hooks/usePermissions.ts @@ -1,39 +1,70 @@ +// // src/hooks/usePermissions.ts +// import { useAuth } from "./useAuth"; + +// export function usePermissions() { +// const { user } = useAuth(); +// const permissions = user?.permissions || {}; + +// const hasPermission = (permission: string): boolean => { +// return !!permissions[permission]; +// }; + +// const hasAnyPermission = (requiredPermissions: string[]): boolean => { +// return requiredPermissions.some((permission) => hasPermission(permission)); +// }; + +// const hasAllPermissions = (requiredPermissions: string[]): boolean => { +// return requiredPermissions.every((permission) => hasPermission(permission)); +// }; + +// return { +// permissions, +// hasPermission, +// hasAnyPermission, +// hasAllPermissions, +// }; +// } + // src/hooks/usePermissions.ts import { useAuth } from "./useAuth"; +import { PATH_PERMISSIONS } from "@/config/permissions"; export function usePermissions() { const { user } = useAuth(); const permissions = user?.permissions || {}; const hasPermission = (permission: string): boolean => { + if (!user) return false; + + // 슈퍼 관리자는 모든 권한을 가짐 + if (user.role === "super_admin") return true; + + // 회사 관리자는 회사 관련 모든 권한을 가짐 + if (user.role === "company_admin") { + if ( + permission.startsWith("company:") || + permission.startsWith("branches:") || + permission.startsWith("users:") || + permission.startsWith("departments:") + ) { + return true; + } + } + return !!permissions[permission]; }; - const hasAnyPermission = (requiredPermissions: string[]): boolean => { + const hasPathPermission = (path: string): boolean => { + const requiredPermissions = PATH_PERMISSIONS[path]; + if (!requiredPermissions) return true; return requiredPermissions.some((permission) => hasPermission(permission)); }; - const hasAllPermissions = (requiredPermissions: string[]): boolean => { - return requiredPermissions.every((permission) => hasPermission(permission)); - }; - return { permissions, hasPermission, - hasAnyPermission, - hasAllPermissions, + hasPathPermission, + hasAnyPermission: (perms: string[]) => perms.some(hasPermission), + hasAllPermissions: (perms: string[]) => perms.every(hasPermission), }; } - -// 사용 예시: -/* -function MyComponent() { - const { hasPermission } = usePermissions(); - - if (!hasPermission('departments:view')) { - return <div>접근 권한이 없습니다.</div>; - } - - return <div>부서 목록...</div>; -} -*/ diff --git a/fems-app/src/types/index.ts b/fems-app/src/types/index.ts index 9934653..bffc62d 100644 --- a/fems-app/src/types/index.ts +++ b/fems-app/src/types/index.ts @@ -1,15 +1,15 @@ // src/types/index.ts export interface User { - id: string; - username: string; - role: 'admin' | 'user'; - // ... - } - - export interface EnergyUsage { - timestamp: string; - value: number; - type: 'electricity' | 'gas' | 'water' | 'steam'; - } - - // ... \ No newline at end of file + id: string; + username: string; + role: "super_admin" | "company_admin" | "branch_admin" | "user"; + permissions: Record<string, boolean>; +} + +export interface EnergyUsage { + timestamp: string; + value: number; + type: "electricity" | "gas" | "water" | "steam"; +} + +// ...