Enhance user management with SUPER_ADMIN access control

- Updated the user list retrieval logic to ensure proper filtering based on company codes, enhancing security for user data access.
- Implemented checks to restrict access to company management APIs, allowing only SUPER_ADMIN users to perform actions related to company data.
- Adjusted the user interface to reflect access restrictions for non-SUPER_ADMIN users, providing clear feedback when access is denied.

These changes strengthen the integrity of user management and ensure that sensitive company information is only accessible to authorized personnel.
This commit is contained in:
kjs
2026-04-01 15:49:49 +09:00
parent 369a201832
commit 2ff01456dc
11 changed files with 346 additions and 149 deletions

View File

@@ -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]

View File

@@ -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);

View File

@@ -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

View File

@@ -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 (
<div className="flex h-full flex-col bg-background">
<div className="space-y-6 p-6">
<div className="space-y-2 border-b pb-4">
<h1 className="text-3xl font-bold tracking-tight"> </h1>
<p className="text-sm text-muted-foreground"> </p>
</div>
<div className="bg-card flex h-64 flex-col items-center justify-center rounded-lg border p-6 shadow-sm">
<AlertCircle className="text-destructive mb-4 h-12 w-12" />
<h3 className="mb-2 text-lg font-semibold"> </h3>
<p className="text-muted-foreground mb-4 text-center text-sm">
.
</p>
<Button variant="outline" onClick={() => window.history.back()}>
</Button>
</div>
</div>
</div>
);
}
return (
<div className="flex h-full flex-col overflow-auto bg-background">
<div className="space-y-6 p-4 sm:p-6 lg:p-8">

View File

@@ -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<Array<{ id: string; name: string; dept?: string }>>([]);
const [selectedUsers, setSelectedUsers] = useState<Array<{ id: string; name: string; dept?: string }>>([]);
const [availableDepts, setAvailableDepts] = useState<Array<{ id: string; label: string; description?: string; userIds: string[] }>>([]);
const [selectedDepts, setSelectedDepts] = useState<Array<{ id: string; label: string; description?: string; userIds: string[] }>>([]);
const [allUsersMap, setAllUsersMap] = useState<Map<string, { id: string; label: string; description?: string }>>(new Map());
const [isSavingMembers, setIsSavingMembers] = useState(false);
// 메뉴 권한 상태
@@ -86,32 +90,52 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin
// 2. 전체 사용자 목록 조회 (같은 회사)
const userAPI = await import("@/lib/api/user");
console.log("🔍 사용자 목록 조회 요청:", {
const usersResponse = await userAPI.userAPI.getList({
companyCode: roleGroup.companyCode,
size: 1000,
});
const usersResponse = await userAPI.userAPI.getList({
companyCode: roleGroup.companyCode,
size: 1000, // 대량 조회
});
console.log("✅ 사용자 목록 응답:", {
success: usersResponse.success,
count: usersResponse.data?.length,
total: usersResponse.total,
});
if (usersResponse.success && usersResponse.data) {
setAvailableUsers(
usersResponse.data.map((user: any) => ({
id: user.userId,
label: user.userName || user.userId,
description: user.deptName,
})),
);
console.log("📋 설정된 전체 사용자 수:", usersResponse.data.length);
const userItems = usersResponse.data.map((user: any) => ({
id: user.userId,
label: user.userName || user.userId,
description: user.deptName,
}));
setAvailableUsers(userItems);
// 전체 사용자 맵 저장 (부서별 추가 시 사용)
const uMap = new Map<string, { id: string; label: string; description?: string }>();
userItems.forEach((u: any) => uMap.set(u.id, u));
setAllUsersMap(uMap);
// 3. 부서 목록 로드 + 부서별 사용자 수 계산
const { getDepartments } = await import("@/lib/api/department");
const deptsResponse = await getDepartments(roleGroup.companyCode);
const depts = deptsResponse?.success ? deptsResponse.data : deptsResponse;
if (Array.isArray(depts)) {
// 부서별 사용자 그룹핑
const deptUserMap = new Map<string, string[]>();
usersResponse.data.forEach((user: any) => {
const deptCode = user.deptCode || user.dept_code;
if (deptCode) {
if (!deptUserMap.has(deptCode)) deptUserMap.set(deptCode, []);
deptUserMap.get(deptCode)!.push(user.userId);
}
});
setAvailableDepts(
depts.map((dept: any) => {
const deptCode = dept.deptCode || dept.dept_code;
const userIds = deptUserMap.get(deptCode) || [];
return {
id: `dept_${deptCode}`,
label: dept.deptName || dept.dept_name || deptCode,
description: `${userIds.length}`,
userIds,
};
}).filter((d: any) => d.userIds.length > 0),
);
}
}
} catch (err) {
console.error("멤버 목록 로드 오류:", err);
@@ -164,6 +188,41 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin
}
}, [roleGroup, activeTab, loadMembers, loadMenuPermissions]);
// 부서 선택 변경 시 우측에 부서 표시 + 해당 사용자를 멤버에 추가
const handleDeptSelectionChange = useCallback(
(newSelectedDepts: Array<{ id: string; label: string; description?: string; [key: string]: any }>) => {
// 새로 추가된 부서 찾기
const prevIds = new Set(selectedDepts.map((d) => d.id));
const addedDepts = newSelectedDepts.filter((d) => !prevIds.has(d.id));
// 우측 부서 목록 업데이트
setSelectedDepts(newSelectedDepts as typeof selectedDepts);
// 새로 추가된 부서의 사용자만 멤버에 추가
if (addedDepts.length > 0) {
const userIdsToAdd = new Set<string>();
addedDepts.forEach((deptItem) => {
const fullDept = availableDepts.find((d) => d.id === deptItem.id);
fullDept?.userIds.forEach((uid) => userIdsToAdd.add(uid));
});
const existingIds = new Set(selectedUsers.map((u) => u.id));
const newUsers: Array<{ id: string; label: string; description?: string }> = [];
userIdsToAdd.forEach((uid) => {
if (!existingIds.has(uid)) {
const userInfo = allUsersMap.get(uid);
if (userInfo) newUsers.push(userInfo);
}
});
if (newUsers.length > 0) {
setSelectedUsers((prev) => [...prev, ...newUsers]);
}
}
},
[availableDepts, selectedDepts, selectedUsers, allUsersMap],
);
// 멤버 저장 핸들러
const handleSaveMembers = useCallback(async () => {
if (!roleGroup) return;
@@ -302,20 +361,84 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin
<h2 className="text-xl font-semibold"> </h2>
<p className="text-muted-foreground text-sm"> </p>
</div>
<Button onClick={handleSaveMembers} disabled={isSavingMembers} className="gap-2">
<Save className="h-4 w-4" />
{isSavingMembers ? "저장 중..." : "멤버 저장"}
</Button>
<div className="flex items-center gap-3">
{/* 사용자별/부서별 모드 전환 */}
<div className="flex rounded-lg border p-0.5">
<button
onClick={() => setMemberMode("user")}
className={`flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors ${
memberMode === "user" ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground"
}`}
>
<UserIcon className="h-3.5 w-3.5" />
</button>
<button
onClick={() => setMemberMode("dept")}
className={`flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors ${
memberMode === "dept" ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground"
}`}
>
<Building2 className="h-3.5 w-3.5" />
</button>
</div>
<Button onClick={handleSaveMembers} disabled={isSavingMembers} className="gap-2">
<Save className="h-4 w-4" />
{isSavingMembers ? "저장 중..." : "멤버 저장"}
</Button>
</div>
</div>
<DualListBox
availableItems={availableUsers}
selectedItems={selectedUsers}
onSelectionChange={setSelectedUsers}
availableLabel="전체 사용자"
selectedLabel="그룹 멤버"
enableSearch
/>
{memberMode === "user" ? (
<DualListBox
availableItems={availableUsers}
selectedItems={selectedUsers}
onSelectionChange={setSelectedUsers}
availableLabel="전체 사용자"
selectedLabel="그룹 멤버"
enableSearch
/>
) : (
<>
<DualListBox
availableItems={availableDepts}
selectedItems={selectedDepts}
onSelectionChange={handleDeptSelectionChange}
availableLabel="부서 목록 (선택 시 소속 사용자 전체 추가)"
selectedLabel="추가된 부서"
enableSearch
renderItem={(item) => (
<div className="flex flex-col">
<span className="text-sm font-medium">{item.label}</span>
<span className="text-muted-foreground text-xs">{item.description}</span>
</div>
)}
/>
{/* 현재 멤버 요약 */}
<div className="rounded-lg border bg-muted/30 p-4">
<p className="mb-2 text-sm font-semibold"> ({selectedUsers.length})</p>
{selectedUsers.length === 0 ? (
<p className="text-muted-foreground text-sm"> </p>
) : (
<div className="flex flex-wrap gap-1.5">
{selectedUsers.map((user) => (
<span key={user.id} className="inline-flex items-center gap-1 rounded-full bg-background border px-2.5 py-0.5 text-xs">
{user.label}
<button
onClick={() => setSelectedUsers((prev) => prev.filter((u) => u.id !== user.id))}
className="ml-0.5 text-muted-foreground hover:text-destructive"
>
x
</button>
</span>
))}
</div>
)}
</div>
</>
)}
</>
)}

View File

@@ -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],
);
// 관리자가 아니면 접근 제한

View File

@@ -165,6 +165,7 @@ export default function UserAuthPage() {
<UserAuthTable
users={users}
isLoading={isLoading}
isSuperAdmin={isSuperAdmin}
paginationInfo={paginationInfo}
onEditAuth={handleEditAuth}
onPageChange={handlePageChange}

View File

@@ -109,15 +109,14 @@ export default function UserMngPage() {
};
return (
<div className="flex min-h-screen flex-col bg-background">
<div className="space-y-6 p-6">
{/* 페이지 헤더 */}
<div className="flex h-full flex-col bg-background">
{/* 상단 고정: 헤더 + 툴바 */}
<div className="shrink-0 space-y-6 p-6 pb-0">
<div className="space-y-2 border-b pb-4">
<h1 className="text-3xl font-bold tracking-tight"> </h1>
<p className="text-sm text-muted-foreground"> </p>
</div>
{/* 툴바 - 검색, 필터, 등록 버튼 */}
<UserToolbar
searchFilter={searchFilter}
totalCount={paginationInfo.totalItems}
@@ -126,7 +125,6 @@ export default function UserMngPage() {
onCreateClick={handleCreateUser}
/>
{/* 에러 메시지 */}
{error && (
<div className="border-destructive/50 bg-destructive/10 rounded-lg border p-4">
<div className="flex items-center justify-between">
@@ -142,8 +140,10 @@ export default function UserMngPage() {
<p className="text-destructive/80 mt-1.5 text-sm">{error}</p>
</div>
)}
</div>
{/* 사용자 목록 테이블 */}
{/* 중간: 테이블 (스크롤 영역) */}
<div className="min-h-0 flex-1 overflow-auto px-6 py-6">
<UserTable
users={users}
isLoading={isLoading}
@@ -152,38 +152,35 @@ export default function UserMngPage() {
onPasswordReset={handlePasswordReset}
onEdit={handleEditUser}
/>
</div>
{/* 페이지네이션 */}
{!isLoading && users.length > 0 && (
{/* 하단 고정: 페이지네이션 */}
{!isLoading && users.length > 0 && (
<div className="shrink-0 border-t px-6 py-3">
<Pagination
paginationInfo={paginationInfo}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
showPageSizeSelector={true}
pageSizeOptions={[10, 20, 50, 100]}
className="mt-6"
/>
)}
</div>
)}
{/* 사용자 등록/수정 모달 */}
<UserFormModal
isOpen={userFormModal.isOpen}
onClose={handleUserFormClose}
onSuccess={handleUserFormSuccess}
editingUser={userFormModal.editingUser}
/>
{/* 비밀번호 초기화 모달 */}
<UserPasswordResetModal
isOpen={passwordResetModal.isOpen}
onClose={handlePasswordResetClose}
userId={passwordResetModal.userId}
userName={passwordResetModal.userName}
onSuccess={handlePasswordResetSuccess}
/>
</div>
{/* Scroll to Top 버튼 (모바일/태블릿 전용) */}
{/* 모달 */}
<UserFormModal
isOpen={userFormModal.isOpen}
onClose={handleUserFormClose}
onSuccess={handleUserFormSuccess}
editingUser={userFormModal.editingUser}
/>
<UserPasswordResetModal
isOpen={passwordResetModal.isOpen}
onClose={handlePasswordResetClose}
userId={passwordResetModal.userId}
userName={passwordResetModal.userName}
onSuccess={handlePasswordResetSuccess}
/>
<ScrollToTop />
</div>
);

View File

@@ -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) => <span>{row.companyName || row.companyCode}</span>,
},
...(isSuperAdmin
? [
{
key: "companyName",
label: "회사",
hideOnMobile: true,
render: (_value: any, row: any) => <span>{row.companyName || row.companyCode}</span>,
} as RDVColumn<any>,
]
: []),
{
key: "deptName",
label: "부서",
@@ -120,10 +125,14 @@ export function UserAuthTable({ users, isLoading, paginationInfo, onEditAuth, on
// 모바일 카드 필드 정의
const cardFields: RDVCardField<any>[] = [
{
label: "회사",
render: (user) => <span>{user.companyName || user.companyCode}</span>,
},
...(isSuperAdmin
? [
{
label: "회사",
render: (user: any) => <span>{user.companyName || user.companyCode}</span>,
} as RDVCardField<any>,
]
: []),
{
label: "부서",
render: (user) => <span>{user.deptName || "-"}</span>,

View File

@@ -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) => <span className="font-mono">{value || "-"}</span>,
},
{
key: "companyCode",
label: "회사",
width: "120px",
hideOnMobile: true,
render: (value) => <span className="font-medium">{value || "-"}</span>,
},
...(isSuperAdmin
? [
{
key: "companyCode" as keyof User,
label: "회사",
width: "120px",
hideOnMobile: true,
render: (value: any, user: User) => (
<span className="font-medium">{(user as any).companyName || value || "-"}</span>
),
},
]
: []),
{
key: "deptName",
label: "부서명",
@@ -202,11 +212,17 @@ export function UserTable({
render: (user) => <span className="font-mono font-medium">{user.sabun || "-"}</span>,
hideEmpty: true,
},
{
label: "회사",
render: (user) => <span className="font-medium">{user.companyCode || ""}</span>,
hideEmpty: true,
},
...(isSuperAdmin
? [
{
label: "회사",
render: (user: User) => (
<span className="font-medium">{(user as any).companyName || user.companyCode || ""}</span>
),
hideEmpty: true,
},
]
: []),
{
label: "부서",
render: (user) => <span className="font-medium">{user.deptName || ""}</span>,

View File

@@ -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") {