feat: 관리자 테이블 스타일 개선 및 탭 컴포넌트 디자인 수정

- 외부 커넥션 관리 테이블 표준화 (DB 연결, REST API 연결)
- 모든 관리자 테이블의 그림자 제거 (테이블 타입 관리 왼쪽 카드 제외)
- 테이블 타입 관리 왼쪽 카드 호버 효과 강화 (shadow-lg, bg-muted/20)
- 탭 컴포넌트 배경색 밝게 조정 (bg-muted/30)
- 탭 트리거 테두리 제거
This commit is contained in:
kjs
2025-10-30 17:55:55 +09:00
parent 4924fbe71d
commit 148155e6fe
16 changed files with 560 additions and 177 deletions

View File

@@ -25,19 +25,22 @@ export async function getAdminMenus(
const userType = req.user?.userType;
const userLang = (req.query.userLang as string) || "ko";
const menuType = req.query.menuType as string | undefined; // menuType 파라미터 추가
const includeInactive = req.query.includeInactive === "true"; // includeInactive 파라미터 추가
logger.info(`사용자 ID: ${userId}`);
logger.info(`사용자 회사 코드: ${userCompanyCode}`);
logger.info(`사용자 유형: ${userType}`);
logger.info(`사용자 로케일: ${userLang}`);
logger.info(`메뉴 타입: ${menuType || "전체"}`);
logger.info(`비활성 메뉴 포함: ${includeInactive}`);
const paramMap = {
userId,
userCompanyCode,
userType,
userLang,
menuType, // menuType 추가
menuType, // includeInactive와 관계없이 menuType 유지 (관리자/사용자 구분)
includeInactive, // includeInactive 추가
};
const menuList = await AdminService.getAdminMenuList(paramMap);
@@ -1081,9 +1084,41 @@ export async function saveMenu(
return;
}
const userCompanyCode = req.user.companyCode;
const userType = req.user.userType;
let requestCompanyCode = menuData.companyCode || menuData.company_code;
// "none"이나 빈 값은 undefined로 처리하여 사용자 회사 코드 사용
if (requestCompanyCode === "none" || requestCompanyCode === "" || !requestCompanyCode) {
requestCompanyCode = undefined;
}
// 공통 메뉴(company_code = '*')는 최고 관리자만 생성 가능
if (requestCompanyCode === "*") {
if (userCompanyCode !== "*" || userType !== "SUPER_ADMIN") {
res.status(403).json({
success: false,
message: "공통 메뉴는 최고 관리자만 생성할 수 있습니다.",
error: "Unauthorized to create common menu",
});
return;
}
} else if (userCompanyCode !== "*") {
// 회사 관리자는 자기 회사 메뉴만 생성 가능
// requestCompanyCode가 undefined면 사용자 회사 코드 사용 (권한 체크 통과)
if (requestCompanyCode && requestCompanyCode !== userCompanyCode) {
res.status(403).json({
success: false,
message: "해당 회사의 메뉴를 생성할 권한이 없습니다.",
error: "Unauthorized to create menu for this company",
});
return;
}
}
// Raw Query를 사용한 메뉴 저장
const objid = Date.now(); // 고유 ID 생성
const companyCode = req.user.companyCode;
const companyCode = requestCompanyCode || userCompanyCode;
const [savedMenu] = await query<any>(
`INSERT INTO menu_info (
@@ -1164,7 +1199,73 @@ export async function updateMenu(
return;
}
const companyCode = req.user.companyCode;
const userCompanyCode = req.user.companyCode;
const userType = req.user.userType;
// 수정하려는 메뉴 조회
const currentMenu = await queryOne<any>(
`SELECT objid, company_code FROM menu_info WHERE objid = $1`,
[Number(menuId)]
);
if (!currentMenu) {
res.status(404).json({
success: false,
message: `메뉴를 찾을 수 없습니다: ${menuId}`,
error: "Menu not found",
});
return;
}
// 공통 메뉴(company_code = '*')는 최고 관리자만 수정 가능
if (currentMenu.company_code === "*") {
if (userCompanyCode !== "*" || userType !== "SUPER_ADMIN") {
res.status(403).json({
success: false,
message: "공통 메뉴는 최고 관리자만 수정할 수 있습니다.",
error: "Unauthorized to update common menu",
});
return;
}
} else if (userCompanyCode !== "*") {
// 회사 관리자는 자기 회사 메뉴만 수정 가능
if (currentMenu.company_code !== userCompanyCode) {
res.status(403).json({
success: false,
message: "해당 회사의 메뉴를 수정할 권한이 없습니다.",
error: "Unauthorized to update menu for this company",
});
return;
}
}
const requestCompanyCode = menuData.companyCode || menuData.company_code || currentMenu.company_code;
// company_code 변경 시도하는 경우 권한 체크
if (requestCompanyCode !== currentMenu.company_code) {
// 공통 메뉴로 변경하려는 경우 최고 관리자만 가능
if (requestCompanyCode === "*") {
if (userCompanyCode !== "*" || userType !== "SUPER_ADMIN") {
res.status(403).json({
success: false,
message: "공통 메뉴로 변경할 권한이 없습니다.",
error: "Unauthorized to change to common menu",
});
return;
}
}
// 회사 관리자는 자기 회사로만 변경 가능
else if (userCompanyCode !== "*" && requestCompanyCode !== userCompanyCode) {
res.status(403).json({
success: false,
message: "해당 회사로 변경할 권한이 없습니다.",
error: "Unauthorized to change company",
});
return;
}
}
const companyCode = requestCompanyCode;
// Raw Query를 사용한 메뉴 수정
const [updatedMenu] = await query<any>(
@@ -1239,6 +1340,56 @@ export async function deleteMenu(
const { menuId } = req.params;
logger.info(`메뉴 삭제 요청: menuId = ${menuId}`, { user: req.user });
// 사용자의 company_code 확인
if (!req.user?.companyCode) {
res.status(400).json({
success: false,
message: "사용자의 회사 코드를 찾을 수 없습니다.",
error: "Missing company_code",
});
return;
}
const userCompanyCode = req.user.companyCode;
const userType = req.user.userType;
// 삭제하려는 메뉴 조회
const currentMenu = await queryOne<any>(
`SELECT objid, company_code FROM menu_info WHERE objid = $1`,
[Number(menuId)]
);
if (!currentMenu) {
res.status(404).json({
success: false,
message: `메뉴를 찾을 수 없습니다: ${menuId}`,
error: "Menu not found",
});
return;
}
// 공통 메뉴(company_code = '*')는 최고 관리자만 삭제 가능
if (currentMenu.company_code === "*") {
if (userCompanyCode !== "*" || userType !== "SUPER_ADMIN") {
res.status(403).json({
success: false,
message: "공통 메뉴는 최고 관리자만 삭제할 수 있습니다.",
error: "Unauthorized to delete common menu",
});
return;
}
} else if (userCompanyCode !== "*") {
// 회사 관리자는 자기 회사 메뉴만 삭제 가능
if (currentMenu.company_code !== userCompanyCode) {
res.status(403).json({
success: false,
message: "해당 회사의 메뉴를 삭제할 권한이 없습니다.",
error: "Unauthorized to delete menu for this company",
});
return;
}
}
// Raw Query를 사용한 메뉴 삭제
const [deletedMenu] = await query<any>(
`DELETE FROM menu_info WHERE objid = $1 RETURNING *`,
@@ -1292,6 +1443,51 @@ export async function deleteMenusBatch(
return;
}
// 사용자의 company_code 확인
if (!req.user?.companyCode) {
res.status(400).json({
success: false,
message: "사용자의 회사 코드를 찾을 수 없습니다.",
error: "Missing company_code",
});
return;
}
const userCompanyCode = req.user.companyCode;
const userType = req.user.userType;
// 삭제하려는 메뉴들의 company_code 확인
const menusToDelete = await query<any>(
`SELECT objid, company_code FROM menu_info WHERE objid = ANY($1::bigint[])`,
[menuIds.map((id) => Number(id))]
);
// 권한 체크: 공통 메뉴 포함 여부 확인
const hasCommonMenu = menusToDelete.some((menu: any) => menu.company_code === "*");
if (hasCommonMenu && (userCompanyCode !== "*" || userType !== "SUPER_ADMIN")) {
res.status(403).json({
success: false,
message: "공통 메뉴는 최고 관리자만 삭제할 수 있습니다.",
error: "Unauthorized to delete common menu",
});
return;
}
// 회사 관리자는 자기 회사 메뉴만 삭제 가능
if (userCompanyCode !== "*") {
const unauthorizedMenus = menusToDelete.filter(
(menu: any) => menu.company_code !== userCompanyCode && menu.company_code !== "*"
);
if (unauthorizedMenus.length > 0) {
res.status(403).json({
success: false,
message: "다른 회사의 메뉴를 삭제할 권한이 없습니다.",
error: "Unauthorized to delete menus for other companies",
});
return;
}
}
// Raw Query를 사용한 메뉴 일괄 삭제
let deletedCount = 0;
let failedCount = 0;
@@ -1354,6 +1550,103 @@ export async function deleteMenusBatch(
}
}
/**
* 메뉴 활성/비활성 토글
*/
export async function toggleMenuStatus(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { menuId } = req.params;
logger.info(`메뉴 상태 토글 요청: menuId = ${menuId}`, { user: req.user });
// 사용자의 company_code 확인
if (!req.user?.companyCode) {
res.status(400).json({
success: false,
message: "사용자의 회사 코드를 찾을 수 없습니다.",
error: "Missing company_code",
});
return;
}
const userCompanyCode = req.user.companyCode;
const userType = req.user.userType;
// 현재 상태 및 회사 코드 조회
const currentMenu = await queryOne<any>(
`SELECT objid, status, company_code FROM menu_info WHERE objid = $1`,
[Number(menuId)]
);
if (!currentMenu) {
res.status(404).json({
success: false,
message: `메뉴를 찾을 수 없습니다: ${menuId}`,
error: "Menu not found",
});
return;
}
// 공통 메뉴(company_code = '*')는 최고 관리자만 상태 변경 가능
if (currentMenu.company_code === "*") {
if (userCompanyCode !== "*" || userType !== "SUPER_ADMIN") {
res.status(403).json({
success: false,
message: "공통 메뉴는 최고 관리자만 상태를 변경할 수 있습니다.",
error: "Unauthorized to toggle common menu status",
});
return;
}
} else if (userCompanyCode !== "*") {
// 회사 관리자는 자기 회사 메뉴만 상태 변경 가능
if (currentMenu.company_code !== userCompanyCode) {
res.status(403).json({
success: false,
message: "해당 회사의 메뉴 상태를 변경할 권한이 없습니다.",
error: "Unauthorized to toggle menu status for this company",
});
return;
}
}
// 상태 토글 (active <-> inactive)
const currentStatus = currentMenu.status;
const newStatus = currentStatus === "active" ? "inactive" : "active";
// 상태 업데이트
const [updatedMenu] = await query<any>(
`UPDATE menu_info SET status = $1 WHERE objid = $2 RETURNING *`,
[newStatus, Number(menuId)]
);
logger.info("메뉴 상태 토글 성공", {
menuId,
oldStatus: currentStatus,
newStatus,
});
const result = newStatus === "active" ? "활성화" : "비활성화";
const response: ApiResponse<string> = {
success: true,
message: `메뉴가 ${result}되었습니다.`,
data: result,
};
res.status(200).json(response);
} catch (error) {
logger.error("메뉴 상태 토글 실패:", error);
res.status(500).json({
success: false,
message: "메뉴 상태 변경에 실패하였습니다.",
error: error instanceof Error ? error.message : "Unknown error",
errorCode: "MENU_TOGGLE_ERROR",
});
}
}
/**
* 회사 목록 조회 (실제 데이터베이스에서)
*/