diff --git a/backend-node/src/controllers/adminController.ts b/backend-node/src/controllers/adminController.ts
index bea2a69b..1501a2c0 100644
--- a/backend-node/src/controllers/adminController.ts
+++ b/backend-node/src/controllers/adminController.ts
@@ -237,7 +237,7 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
// 회사 코드 필터 (권한 그룹 멤버 관리 시 사용)
if (companyCode && typeof companyCode === "string" && companyCode.trim()) {
- whereConditions.push(`company_code = $${paramIndex}`);
+ whereConditions.push(`u.company_code = $${paramIndex}`);
queryParams.push(companyCode.trim());
paramIndex++;
logger.info("회사 코드 필터 적용", { companyCode });
@@ -246,7 +246,7 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
// 최고 관리자 필터링 (회사 관리자와 일반 사용자는 최고 관리자를 볼 수 없음)
if (req.user && req.user.companyCode !== "*") {
// 최고 관리자가 아닌 경우, company_code가 "*"인 사용자는 제외
- whereConditions.push(`company_code != '*'`);
+ whereConditions.push(`u.company_code != '*'`);
logger.info("최고 관리자 필터링 적용", {
userCompanyCode: req.user.companyCode,
});
@@ -259,15 +259,15 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
const searchTerm = search.trim();
whereConditions.push(`(
- sabun ILIKE $${paramIndex} OR
- user_type_name ILIKE $${paramIndex} OR
- dept_name ILIKE $${paramIndex} OR
- position_name ILIKE $${paramIndex} OR
- user_id ILIKE $${paramIndex} OR
- user_name ILIKE $${paramIndex} OR
- tel ILIKE $${paramIndex} OR
- cell_phone ILIKE $${paramIndex} OR
- email ILIKE $${paramIndex}
+ u.sabun ILIKE $${paramIndex} OR
+ u.user_type_name ILIKE $${paramIndex} OR
+ u.dept_name ILIKE $${paramIndex} OR
+ u.position_name ILIKE $${paramIndex} OR
+ u.user_id ILIKE $${paramIndex} OR
+ u.user_name ILIKE $${paramIndex} OR
+ u.tel ILIKE $${paramIndex} OR
+ u.cell_phone ILIKE $${paramIndex} OR
+ u.email ILIKE $${paramIndex}
)`);
queryParams.push(`%${searchTerm}%`);
paramIndex++;
@@ -277,21 +277,21 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
// 단일 필드 검색
searchType = "single";
const fieldMap: { [key: string]: string } = {
- sabun: "sabun",
- companyName: "user_type_name",
- deptName: "dept_name",
- positionName: "position_name",
- userId: "user_id",
- userName: "user_name",
- tel: "tel",
- cellPhone: "cell_phone",
- email: "email",
+ sabun: "u.sabun",
+ companyName: "u.user_type_name",
+ deptName: "u.dept_name",
+ positionName: "u.position_name",
+ userId: "u.user_id",
+ userName: "u.user_name",
+ tel: "u.tel",
+ cellPhone: "u.cell_phone",
+ email: "u.email",
};
if (fieldMap[searchField as string]) {
if (searchField === "tel") {
whereConditions.push(
- `(tel ILIKE $${paramIndex} OR cell_phone ILIKE $${paramIndex})`
+ `(u.tel ILIKE $${paramIndex} OR u.cell_phone ILIKE $${paramIndex})`
);
queryParams.push(`%${searchValue}%`);
paramIndex++;
@@ -307,13 +307,13 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
} else {
// 고급 검색 (개별 필드별 AND 조건)
const advancedSearchFields = [
- { param: search_sabun, field: "sabun" },
- { param: search_companyName, field: "user_type_name" },
- { param: search_deptName, field: "dept_name" },
- { param: search_positionName, field: "position_name" },
- { param: search_userId, field: "user_id" },
- { param: search_userName, field: "user_name" },
- { param: search_email, field: "email" },
+ { param: search_sabun, field: "u.sabun" },
+ { param: search_companyName, field: "u.user_type_name" },
+ { param: search_deptName, field: "u.dept_name" },
+ { param: search_positionName, field: "u.position_name" },
+ { param: search_userId, field: "u.user_id" },
+ { param: search_userName, field: "u.user_name" },
+ { param: search_email, field: "u.email" },
];
let hasAdvancedSearch = false;
@@ -330,7 +330,7 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
// 전화번호 검색
if (search_tel && typeof search_tel === "string" && search_tel.trim()) {
whereConditions.push(
- `(tel ILIKE $${paramIndex} OR cell_phone ILIKE $${paramIndex})`
+ `(u.tel ILIKE $${paramIndex} OR u.cell_phone ILIKE $${paramIndex})`
);
queryParams.push(`%${search_tel.trim()}%`);
paramIndex++;
@@ -354,7 +354,7 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
// 현재 로그인한 사용자의 회사 코드 필터 (슈퍼관리자가 아닌 경우)
if (req.user && req.user.companyCode !== "*" && !companyCode) {
- whereConditions.push(`company_code = $${paramIndex}`);
+ whereConditions.push(`u.company_code = $${paramIndex}`);
queryParams.push(req.user.companyCode);
paramIndex++;
logger.info("사용자 회사 코드 필터 적용", {
@@ -364,13 +364,13 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
// 기존 필터들
if (deptCode) {
- whereConditions.push(`dept_code = $${paramIndex}`);
+ whereConditions.push(`u.dept_code = $${paramIndex}`);
queryParams.push(deptCode);
paramIndex++;
}
if (status) {
- whereConditions.push(`status = $${paramIndex}`);
+ whereConditions.push(`u.status = $${paramIndex}`);
queryParams.push(status);
paramIndex++;
}
@@ -383,7 +383,7 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
// 총 개수 조회
const countQuery = `
SELECT COUNT(*) as total
- FROM user_info
+ FROM user_info u
${whereClause}
`;
const countResult = await query<{ total: string }>(countQuery, queryParams);
@@ -394,26 +394,28 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
const offset = (Number(page) - 1) * limit;
const usersQuery = `
SELECT
- sabun,
- user_id,
- user_name,
- user_name_eng,
- dept_code,
- dept_name,
- position_code,
- position_name,
- email,
- tel,
- cell_phone,
- user_type,
- user_type_name,
- regdate,
- status,
- company_code,
- locale
- FROM user_info
+ u.sabun,
+ u.user_id,
+ u.user_name,
+ u.user_name_eng,
+ u.dept_code,
+ u.dept_name,
+ u.position_code,
+ u.position_name,
+ u.email,
+ u.tel,
+ u.cell_phone,
+ u.user_type,
+ u.user_type_name,
+ u.regdate,
+ u.status,
+ u.company_code,
+ u.locale,
+ c.company_name
+ FROM user_info u
+ LEFT JOIN company_mng c ON u.company_code = c.company_code
${whereClause}
- ORDER BY regdate DESC, user_name ASC
+ ORDER BY u.regdate DESC, u.user_name ASC
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
`;
@@ -436,6 +438,7 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
userTypeName: user.user_type_name || null,
status: user.status || "active",
companyCode: user.company_code || null,
+ companyName: user.company_name || null,
locale: user.locale || null,
regDate: user.regdate
? new Date(user.regdate).toISOString().split("T")[0]
diff --git a/backend-node/src/routes/adminRoutes.ts b/backend-node/src/routes/adminRoutes.ts
index d0ddbd6c..3a173cbe 100644
--- a/backend-node/src/routes/adminRoutes.ts
+++ b/backend-node/src/routes/adminRoutes.ts
@@ -33,6 +33,7 @@ import {
getTableSchema, // 테이블 스키마 조회
} from "../controllers/adminController";
import { authenticateToken } from "../middleware/authMiddleware";
+import { requireSuperAdmin } from "../middleware/permissionMiddleware";
const router = Router();
@@ -68,13 +69,13 @@ router.delete("/users/:userId", deleteUser); // 사용자 삭제 (soft delete)
// 부서 관리 API
router.get("/departments", getDepartmentList); // 부서 목록 조회
-// 회사 관리 API
-router.get("/companies", getCompanyList);
-router.get("/companies/db", getCompanyListFromDB); // 실제 DB에서 회사 목록 조회
-router.get("/companies/:companyCode", getCompanyByCode); // 회사 단건 조회
-router.post("/companies", createCompany); // 회사 등록
-router.put("/companies/:companyCode", updateCompany); // 회사 수정
-router.delete("/companies/:companyCode", deleteCompany); // 회사 삭제
+// 회사 관리 API (최고관리자 전용)
+router.get("/companies", requireSuperAdmin, getCompanyList);
+router.get("/companies/db", requireSuperAdmin, getCompanyListFromDB);
+router.get("/companies/:companyCode", requireSuperAdmin, getCompanyByCode);
+router.post("/companies", requireSuperAdmin, createCompany);
+router.put("/companies/:companyCode", requireSuperAdmin, updateCompany);
+router.delete("/companies/:companyCode", requireSuperAdmin, deleteCompany);
// 사용자 로케일 API
router.get("/user-locale", getUserLocale);
diff --git a/backend-node/src/routes/companyManagementRoutes.ts b/backend-node/src/routes/companyManagementRoutes.ts
index 630a3234..34b044fc 100644
--- a/backend-node/src/routes/companyManagementRoutes.ts
+++ b/backend-node/src/routes/companyManagementRoutes.ts
@@ -1,5 +1,6 @@
import express from "express";
import { authenticateToken } from "../middleware/authMiddleware";
+import { requireSuperAdmin } from "../middleware/permissionMiddleware";
import { AuthenticatedRequest } from "../types/auth";
import { logger } from "../utils/logger";
import { FileSystemManager } from "../utils/fileSystemManager";
@@ -7,8 +8,9 @@ import { query, queryOne } from "../database/db";
const router = express.Router();
-// 모든 라우트에 인증 미들웨어 적용
+// 모든 라우트에 인증 + 최고관리자 권한 필수
router.use(authenticateToken);
+router.use(requireSuperAdmin);
/**
* DELETE /api/company-management/:companyCode
diff --git a/frontend/app/(main)/admin/userMng/companyList/page.tsx b/frontend/app/(main)/admin/userMng/companyList/page.tsx
index 8c8bd617..1f08f097 100644
--- a/frontend/app/(main)/admin/userMng/companyList/page.tsx
+++ b/frontend/app/(main)/admin/userMng/companyList/page.tsx
@@ -7,12 +7,18 @@ import { CompanyFormModal } from "@/components/admin/CompanyFormModal";
import { CompanyDeleteDialog } from "@/components/admin/CompanyDeleteDialog";
import { DiskUsageSummary } from "@/components/admin/DiskUsageSummary";
import { ScrollToTop } from "@/components/common/ScrollToTop";
+import { useAuth } from "@/hooks/useAuth";
+import { AlertCircle } from "lucide-react";
+import { Button } from "@/components/ui/button";
/**
* 회사 관리 페이지
- * 모든 회사 관리 기능을 통합하여 제공
+ * 최고 관리자만 접근 가능
*/
export default function CompanyPage() {
+ const { user: currentUser } = useAuth();
+ const isSuperAdmin = currentUser?.companyCode === "*" && currentUser?.userType === "SUPER_ADMIN";
+
const {
// 데이터
companies,
@@ -51,6 +57,29 @@ export default function CompanyPage() {
clearError,
} = useCompanyManagement();
+ if (!isSuperAdmin) {
+ return (
+
+
+
+
회사 관리
+
시스템에서 사용하는 회사 정보를 관리합니다
+
+
+
+
접근 권한 없음
+
+ 회사 관리는 최고 관리자만 접근할 수 있습니다.
+
+
+
+
+
+ );
+ }
+
return (
diff --git a/frontend/app/(main)/admin/userMng/rolesList/[id]/page.tsx b/frontend/app/(main)/admin/userMng/rolesList/[id]/page.tsx
index 4609312c..f78b4a37 100644
--- a/frontend/app/(main)/admin/userMng/rolesList/[id]/page.tsx
+++ b/frontend/app/(main)/admin/userMng/rolesList/[id]/page.tsx
@@ -3,7 +3,7 @@
import React, { useState, useCallback, useEffect } from "react";
import { use } from "react";
import { Button } from "@/components/ui/button";
-import { ArrowLeft, Users, Menu as MenuIcon, Save, AlertCircle } from "lucide-react";
+import { ArrowLeft, Users, Menu as MenuIcon, Save, AlertCircle, Building2, User as UserIcon } from "lucide-react";
import { roleAPI, RoleGroup } from "@/lib/api/role";
import { useAuth } from "@/hooks/useAuth";
import { useRouter } from "next/navigation";
@@ -38,8 +38,12 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin
const [activeTab, setActiveTab] = useState<"members" | "permissions">("members");
// 멤버 관리 상태
+ const [memberMode, setMemberMode] = useState<"user" | "dept">("user");
const [availableUsers, setAvailableUsers] = useState
>([]);
const [selectedUsers, setSelectedUsers] = useState>([]);
+ const [availableDepts, setAvailableDepts] = useState>([]);
+ const [selectedDepts, setSelectedDepts] = useState>([]);
+ const [allUsersMap, setAllUsersMap] = useState
-
+
+ {/* 사용자별/부서별 모드 전환 */}
+
+
+
+
+
+
-
+ {memberMode === "user" ? (
+
+ ) : (
+ <>
+ (
+
+ {item.label}
+ {item.description}
+
+ )}
+ />
+
+ {/* 현재 멤버 요약 */}
+
+
현재 그룹 멤버 ({selectedUsers.length}명)
+ {selectedUsers.length === 0 ? (
+
멤버가 없습니다
+ ) : (
+
+ {selectedUsers.map((user) => (
+
+ {user.label}
+
+
+ ))}
+
+ )}
+
+ >
+ )}
>
)}
diff --git a/frontend/app/(main)/admin/userMng/rolesList/page.tsx b/frontend/app/(main)/admin/userMng/rolesList/page.tsx
index 48a4ff32..58ba5359 100644
--- a/frontend/app/(main)/admin/userMng/rolesList/page.tsx
+++ b/frontend/app/(main)/admin/userMng/rolesList/page.tsx
@@ -9,6 +9,7 @@ import { AlertCircle } from "lucide-react";
import { RoleFormModal } from "@/components/admin/RoleFormModal";
import { RoleDeleteModal } from "@/components/admin/RoleDeleteModal";
import { useRouter } from "next/navigation";
+import { useTabStore } from "@/stores/tabStore";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { companyAPI } from "@/lib/api/company";
import { ScrollToTop } from "@/components/common/ScrollToTop";
@@ -29,6 +30,7 @@ import { ScrollToTop } from "@/components/common/ScrollToTop";
export default function RolesPage() {
const { user: currentUser } = useAuth();
const router = useRouter();
+ const openTab = useTabStore((s) => s.openTab);
// 회사 관리자 또는 최고 관리자 여부
const isAdmin =
@@ -147,9 +149,13 @@ export default function RolesPage() {
// 상세 페이지로 이동
const handleViewDetail = useCallback(
(role: RoleGroup) => {
- router.push(`/admin/userMng/rolesList/${role.objid}`);
+ openTab({
+ type: "admin",
+ title: role.authName,
+ adminUrl: `/admin/userMng/rolesList/${role.objid}`,
+ });
},
- [router],
+ [openTab],
);
// 관리자가 아니면 접근 제한
diff --git a/frontend/app/(main)/admin/userMng/userAuthList/page.tsx b/frontend/app/(main)/admin/userMng/userAuthList/page.tsx
index c49adbd6..158693f7 100644
--- a/frontend/app/(main)/admin/userMng/userAuthList/page.tsx
+++ b/frontend/app/(main)/admin/userMng/userAuthList/page.tsx
@@ -165,6 +165,7 @@ export default function UserAuthPage() {
-
- {/* 페이지 헤더 */}
+
+ {/* 상단 고정: 헤더 + 툴바 */}
+
사용자 관리
시스템 사용자 계정 및 권한을 관리합니다
- {/* 툴바 - 검색, 필터, 등록 버튼 */}
- {/* 에러 메시지 */}
{error && (
@@ -142,8 +140,10 @@ export default function UserMngPage() {
{error}
)}
+
- {/* 사용자 목록 테이블 */}
+ {/* 중간: 테이블 (스크롤 영역) */}
+
+
- {/* 페이지네이션 */}
- {!isLoading && users.length > 0 && (
+ {/* 하단 고정: 페이지네이션 */}
+ {!isLoading && users.length > 0 && (
+
+ )}
- {/* 사용자 등록/수정 모달 */}
-
-
- {/* 비밀번호 초기화 모달 */}
-
-
-
- {/* Scroll to Top 버튼 (모바일/태블릿 전용) */}
+ {/* 모달 */}
+
+
);
diff --git a/frontend/components/admin/UserAuthTable.tsx b/frontend/components/admin/UserAuthTable.tsx
index 50e7d889..597afcc7 100644
--- a/frontend/components/admin/UserAuthTable.tsx
+++ b/frontend/components/admin/UserAuthTable.tsx
@@ -9,6 +9,7 @@ import { ResponsiveDataView, RDVColumn, RDVCardField } from "@/components/common
interface UserAuthTableProps {
users: any[];
isLoading: boolean;
+ isSuperAdmin?: boolean;
paginationInfo: {
currentPage: number;
pageSize: number;
@@ -24,7 +25,7 @@ interface UserAuthTableProps {
*
* 사용자 목록과 권한 정보를 표시하고 권한 변경 기능 제공
*/
-export function UserAuthTable({ users, isLoading, paginationInfo, onEditAuth, onPageChange }: UserAuthTableProps) {
+export function UserAuthTable({ users, isLoading, isSuperAdmin, paginationInfo, onEditAuth, onPageChange }: UserAuthTableProps) {
// 권한 레벨 표시
const getUserTypeInfo = (userType: string) => {
switch (userType) {
@@ -90,12 +91,16 @@ export function UserAuthTable({ users, isLoading, paginationInfo, onEditAuth, on
key: "userName",
label: "사용자명",
},
- {
- key: "companyName",
- label: "회사",
- hideOnMobile: true,
- render: (_value, row) =>
{row.companyName || row.companyCode},
- },
+ ...(isSuperAdmin
+ ? [
+ {
+ key: "companyName",
+ label: "회사",
+ hideOnMobile: true,
+ render: (_value: any, row: any) =>
{row.companyName || row.companyCode},
+ } as RDVColumn
,
+ ]
+ : []),
{
key: "deptName",
label: "부서",
@@ -120,10 +125,14 @@ export function UserAuthTable({ users, isLoading, paginationInfo, onEditAuth, on
// 모바일 카드 필드 정의
const cardFields: RDVCardField[] = [
- {
- label: "회사",
- render: (user) => {user.companyName || user.companyCode},
- },
+ ...(isSuperAdmin
+ ? [
+ {
+ label: "회사",
+ render: (user: any) => {user.companyName || user.companyCode},
+ } as RDVCardField,
+ ]
+ : []),
{
label: "부서",
render: (user) => {user.deptName || "-"},
diff --git a/frontend/components/admin/UserTable.tsx b/frontend/components/admin/UserTable.tsx
index a0073f4f..84946f85 100644
--- a/frontend/components/admin/UserTable.tsx
+++ b/frontend/components/admin/UserTable.tsx
@@ -7,6 +7,7 @@ import { PaginationInfo } from "@/components/common/Pagination";
import { ResponsiveDataView, RDVColumn, RDVCardField } from "@/components/common/ResponsiveDataView";
import { UserStatusConfirmDialog } from "./UserStatusConfirmDialog";
import { UserHistoryModal } from "./UserHistoryModal";
+import { useAuth } from "@/hooks/useAuth";
interface UserTableProps {
users: User[];
@@ -28,6 +29,9 @@ export function UserTable({
onPasswordReset,
onEdit,
}: UserTableProps) {
+ const { user: currentUser } = useAuth();
+ const isSuperAdmin = currentUser?.companyCode === "*" && currentUser?.userType === "SUPER_ADMIN";
+
// 확인 모달 상태 관리
const [confirmDialog, setConfirmDialog] = useState<{
isOpen: boolean;
@@ -119,13 +123,19 @@ export function UserTable({
hideOnMobile: true,
render: (value) => {value || "-"},
},
- {
- key: "companyCode",
- label: "회사",
- width: "120px",
- hideOnMobile: true,
- render: (value) => {value || "-"},
- },
+ ...(isSuperAdmin
+ ? [
+ {
+ key: "companyCode" as keyof User,
+ label: "회사",
+ width: "120px",
+ hideOnMobile: true,
+ render: (value: any, user: User) => (
+ {(user as any).companyName || value || "-"}
+ ),
+ },
+ ]
+ : []),
{
key: "deptName",
label: "부서명",
@@ -202,11 +212,17 @@ export function UserTable({
render: (user) => {user.sabun || "-"},
hideEmpty: true,
},
- {
- label: "회사",
- render: (user) => {user.companyCode || ""},
- hideEmpty: true,
- },
+ ...(isSuperAdmin
+ ? [
+ {
+ label: "회사",
+ render: (user: User) => (
+ {(user as any).companyName || user.companyCode || ""}
+ ),
+ hideEmpty: true,
+ },
+ ]
+ : []),
{
label: "부서",
render: (user) => {user.deptName || ""},
diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx
index d82d44f0..faa38879 100644
--- a/frontend/components/layout/AppLayout.tsx
+++ b/frontend/components/layout/AppLayout.tsx
@@ -100,9 +100,19 @@ const getMenuIcon = (menuName: string, dbIconName?: string | null) => {
};
const convertMenuToUI = (menus: MenuItem[], userInfo: ExtendedUserInfo | null, parentId: string = "0", parentPath: string = ""): any[] => {
+ const isSuperAdmin = userInfo?.companyCode === "*" && userInfo?.userType === "SUPER_ADMIN";
+
const filteredMenus = menus
.filter((menu) => (menu.parent_obj_id || menu.PARENT_OBJ_ID) === parentId)
.filter((menu) => (menu.status || menu.STATUS) === "active")
+ .filter((menu) => {
+ // 회사관리 메뉴는 최고관리자만 표시
+ const url = (menu.menu_url || menu.MENU_URL || "").toLowerCase();
+ if (url.includes("companylist") || url.includes("company-list")) {
+ return isSuperAdmin;
+ }
+ return true;
+ })
.sort((a, b) => (a.seq || a.SEQ || 0) - (b.seq || b.SEQ || 0));
if (parentId === "0") {