feat: 관리자 테이블 스타일 개선 및 탭 컴포넌트 디자인 수정
- 외부 커넥션 관리 테이블 표준화 (DB 연결, REST API 연결) - 모든 관리자 테이블의 그림자 제거 (테이블 타입 관리 왼쪽 카드 제외) - 테이블 타입 관리 왼쪽 카드 호버 효과 강화 (shadow-lg, bg-muted/20) - 탭 컴포넌트 배경색 밝게 조정 (bg-muted/30) - 탭 트리거 테두리 제거
This commit is contained in:
parent
4924fbe71d
commit
148155e6fe
|
|
@ -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",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 회사 목록 조회 (실제 데이터베이스에서)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
updateMenu, // 메뉴 수정
|
||||
deleteMenu, // 메뉴 삭제
|
||||
deleteMenusBatch, // 메뉴 일괄 삭제
|
||||
toggleMenuStatus, // 메뉴 상태 토글
|
||||
getUserList,
|
||||
getUserInfo, // 사용자 상세 조회
|
||||
getUserHistory, // 사용자 변경이력 조회
|
||||
|
|
@ -37,6 +38,7 @@ router.get("/user-menus", getUserMenus);
|
|||
router.get("/menus/:menuId", getMenuInfo);
|
||||
router.post("/menus", saveMenu); // 메뉴 추가
|
||||
router.put("/menus/:menuId", updateMenu); // 메뉴 수정
|
||||
router.put("/menus/:menuId/toggle", toggleMenuStatus); // 메뉴 상태 토글
|
||||
router.delete("/menus/batch", deleteMenusBatch); // 메뉴 일괄 삭제 (순서 중요!)
|
||||
router.delete("/menus/:menuId", deleteMenu); // 메뉴 삭제
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,15 @@ export class AdminService {
|
|||
|
||||
// menuType에 따른 WHERE 조건 생성
|
||||
const menuTypeCondition =
|
||||
menuType !== undefined ? `MENU_TYPE = ${parseInt(menuType)}` : "1 = 1";
|
||||
menuType !== undefined ? `MENU.MENU_TYPE = ${parseInt(menuType)}` : "1 = 1";
|
||||
|
||||
// 메뉴 관리 화면인지 좌측 사이드바인지 구분
|
||||
// includeInactive가 true거나 menuType이 undefined면 메뉴 관리 화면
|
||||
const includeInactive = paramMap.includeInactive === true;
|
||||
const isManagementScreen = includeInactive || menuType === undefined;
|
||||
// 메뉴 관리 화면: 모든 상태의 메뉴 표시, 좌측 사이드바: active만 표시
|
||||
const statusCondition = isManagementScreen ? "1 = 1" : "MENU.STATUS = 'active'";
|
||||
const subStatusCondition = isManagementScreen ? "1 = 1" : "MENU_SUB.STATUS = 'active'";
|
||||
|
||||
// 1. 권한 그룹 기반 필터링 (좌측 사이드바인 경우만)
|
||||
let authFilter = "";
|
||||
|
|
@ -27,8 +35,8 @@ export class AdminService {
|
|||
let queryParams: any[] = [userLang];
|
||||
let paramIndex = 2;
|
||||
|
||||
if (menuType !== undefined && userType !== "SUPER_ADMIN") {
|
||||
// 좌측 사이드바 + SUPER_ADMIN이 아닌 경우
|
||||
if (menuType !== undefined && userType !== "SUPER_ADMIN" && !isManagementScreen) {
|
||||
// 좌측 사이드바 + SUPER_ADMIN이 아닌 경우만 권한 체크
|
||||
const userRoleGroups = await query<any>(
|
||||
`
|
||||
SELECT DISTINCT am.objid AS role_objid, am.auth_name
|
||||
|
|
@ -123,7 +131,7 @@ export class AdminService {
|
|||
return [];
|
||||
}
|
||||
}
|
||||
} else if (menuType !== undefined && userType === "SUPER_ADMIN") {
|
||||
} else if (menuType !== undefined && userType === "SUPER_ADMIN" && !isManagementScreen) {
|
||||
// 좌측 사이드바 + SUPER_ADMIN: 권한 그룹 체크 없이 모든 공통 메뉴 표시
|
||||
logger.info(`✅ 최고 관리자는 권한 그룹 체크 없이 모든 공통 메뉴 표시`);
|
||||
// unionFilter는 비워둠 (하위 메뉴도 공통 메뉴만)
|
||||
|
|
@ -136,7 +144,7 @@ export class AdminService {
|
|||
// SUPER_ADMIN과 COMPANY_ADMIN 구분
|
||||
if (userType === "SUPER_ADMIN" && userCompanyCode === "*") {
|
||||
// SUPER_ADMIN
|
||||
if (menuType === undefined) {
|
||||
if (isManagementScreen) {
|
||||
// 메뉴 관리 화면: 모든 메뉴
|
||||
logger.info("✅ 메뉴 관리 화면 (SUPER_ADMIN): 모든 메뉴 표시");
|
||||
companyFilter = "";
|
||||
|
|
@ -145,16 +153,34 @@ export class AdminService {
|
|||
logger.info("✅ 좌측 사이드바 (SUPER_ADMIN): 공통 메뉴만 표시");
|
||||
companyFilter = `AND MENU.COMPANY_CODE = '*'`;
|
||||
}
|
||||
} else if (menuType === undefined) {
|
||||
// 메뉴 관리 화면: 자기 회사 + 공통 메뉴
|
||||
} else if (isManagementScreen) {
|
||||
// 메뉴 관리 화면: 회사별 필터링
|
||||
if (userType === "SUPER_ADMIN" && userCompanyCode === "*") {
|
||||
// 최고 관리자: 모든 메뉴 (공통 + 모든 회사)
|
||||
logger.info("✅ 메뉴 관리 화면 (SUPER_ADMIN): 모든 메뉴 표시");
|
||||
companyFilter = "";
|
||||
} else {
|
||||
// 회사 관리자: 자기 회사 메뉴만 (공통 메뉴 제외)
|
||||
logger.info(
|
||||
`✅ 메뉴 관리 화면 (${userType}): 회사 ${userCompanyCode} 메뉴만 표시 (공통 메뉴 제외)`
|
||||
);
|
||||
companyFilter = `AND MENU.COMPANY_CODE = $${paramIndex}`;
|
||||
queryParams.push(userCompanyCode);
|
||||
paramIndex++;
|
||||
|
||||
// 하위 메뉴에도 회사 필터링 적용 (공통 메뉴 제외)
|
||||
if (unionFilter === "") {
|
||||
unionFilter = `AND MENU_SUB.COMPANY_CODE = $${paramIndex - 1}`;
|
||||
}
|
||||
}
|
||||
} else if (menuType !== undefined) {
|
||||
// 좌측 사이드바: authFilter에서 이미 회사 필터링 포함
|
||||
// 회사 관리자는 좌측 사이드바에서도 자기 회사 메뉴 조회 가능
|
||||
logger.info(
|
||||
`✅ 메뉴 관리 화면: 회사 ${userCompanyCode} + 공통 메뉴 표시`
|
||||
`✅ 좌측 사이드바: 회사 ${userCompanyCode} 메뉴 표시 (${userType})`
|
||||
);
|
||||
companyFilter = `AND (MENU.COMPANY_CODE = $${paramIndex} OR MENU.COMPANY_CODE = '*')`;
|
||||
queryParams.push(userCompanyCode);
|
||||
paramIndex++;
|
||||
// companyFilter는 authFilter에서 이미 처리됨
|
||||
}
|
||||
// menuType이 정의된 경우 (좌측 사이드바)는 authFilter에서 이미 회사 필터링 포함
|
||||
|
||||
// 기존 Java의 selectAdminMenuList 쿼리를 Raw Query로 포팅
|
||||
// WITH RECURSIVE 쿼리 구현
|
||||
|
|
@ -237,7 +263,7 @@ export class AdminService {
|
|||
)
|
||||
FROM MENU_INFO MENU
|
||||
WHERE ${menuTypeCondition}
|
||||
AND STATUS = 'active'
|
||||
AND ${statusCondition}
|
||||
${companyFilter}
|
||||
${authFilter}
|
||||
AND NOT EXISTS (
|
||||
|
|
@ -304,7 +330,7 @@ export class AdminService {
|
|||
FROM MENU_INFO MENU_SUB
|
||||
JOIN V_MENU ON MENU_SUB.PARENT_OBJ_ID = V_MENU.OBJID
|
||||
WHERE MENU_SUB.OBJID != ANY(V_MENU.PATH)
|
||||
AND MENU_SUB.STATUS = 'active'
|
||||
AND ${subStatusCondition}
|
||||
${unionFilter}
|
||||
)
|
||||
SELECT
|
||||
|
|
|
|||
|
|
@ -302,20 +302,20 @@ export default function ExternalConnectionsPage() {
|
|||
|
||||
{/* 연결 목록 */}
|
||||
{loading ? (
|
||||
<div className="flex h-64 items-center justify-center rounded-lg border bg-card shadow-sm">
|
||||
<div className="flex h-64 items-center justify-center bg-card">
|
||||
<div className="text-sm text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
) : connections.length === 0 ? (
|
||||
<div className="flex h-64 flex-col items-center justify-center rounded-lg border bg-card shadow-sm">
|
||||
<div className="flex h-64 flex-col items-center justify-center bg-card">
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<p className="text-sm text-muted-foreground">등록된 연결이 없습니다</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border bg-card shadow-sm">
|
||||
<div className="bg-card">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableRow className="bg-background">
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">연결명</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">DB 타입</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">호스트:포트</TableHead>
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ export default function WebTypesManagePage() {
|
|||
return (
|
||||
<div className="flex h-96 items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-lg text-red-600">웹타입 목록을 불러오는데 실패했습니다.</div>
|
||||
<div className="mb-2 text-lg text-destructive">웹타입 목록을 불러오는데 실패했습니다.</div>
|
||||
<Button onClick={() => refetch()} variant="outline">
|
||||
다시 시도
|
||||
</Button>
|
||||
|
|
@ -127,13 +127,13 @@ export default function WebTypesManagePage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="w-full max-w-none px-4 py-8 space-y-8">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
|
||||
<div className="flex items-center justify-between bg-background rounded-lg shadow-sm border p-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">웹타입 관리</h1>
|
||||
<p className="mt-2 text-gray-600">화면관리에서 사용할 웹타입들을 관리합니다</p>
|
||||
<h1 className="text-3xl font-bold text-foreground">웹타입 관리</h1>
|
||||
<p className="mt-2 text-muted-foreground">화면관리에서 사용할 웹타입들을 관리합니다</p>
|
||||
</div>
|
||||
<Link href="/admin/standards/new">
|
||||
<Button className="shadow-sm">
|
||||
|
|
@ -144,9 +144,9 @@ export default function WebTypesManagePage() {
|
|||
|
||||
{/* 필터 및 검색 */}
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader className="bg-gray-50/50">
|
||||
<CardHeader className="bg-muted/50">
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<Filter className="h-5 w-5 text-gray-600" />
|
||||
<Filter className="h-5 w-5 text-muted-foreground" />
|
||||
필터 및 검색
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
|
@ -154,7 +154,7 @@ export default function WebTypesManagePage() {
|
|||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
{/* 검색 */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="웹타입명, 설명 검색..."
|
||||
value={searchTerm}
|
||||
|
|
@ -200,74 +200,73 @@ export default function WebTypesManagePage() {
|
|||
</Card>
|
||||
|
||||
{/* 결과 통계 */}
|
||||
<div className="bg-white rounded-lg border px-4 py-3">
|
||||
<p className="text-gray-700 text-sm font-medium">총 {filteredAndSortedWebTypes.length}개의 웹타입이 있습니다.</p>
|
||||
<div className="bg-background rounded-lg border px-4 py-3">
|
||||
<p className="text-foreground text-sm font-medium">총 {filteredAndSortedWebTypes.length}개의 웹타입이 있습니다.</p>
|
||||
</div>
|
||||
|
||||
{/* 웹타입 목록 테이블 */}
|
||||
<Card className="shadow-sm">
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("sort_order")}>
|
||||
<div className="flex items-center gap-2">
|
||||
순서
|
||||
{sortField === "sort_order" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("web_type")}>
|
||||
<div className="flex items-center gap-2">
|
||||
웹타입 코드
|
||||
{sortField === "web_type" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("type_name")}>
|
||||
<div className="flex items-center gap-2">
|
||||
웹타입명
|
||||
{sortField === "type_name" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("category")}>
|
||||
<div className="flex items-center gap-2">
|
||||
카테고리
|
||||
{sortField === "category" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead>설명</TableHead>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("component_name")}>
|
||||
<div className="flex items-center gap-2">
|
||||
연결된 컴포넌트
|
||||
{sortField === "component_name" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("config_panel")}>
|
||||
<div className="flex items-center gap-2">
|
||||
설정 패널
|
||||
{sortField === "config_panel" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("is_active")}>
|
||||
<div className="flex items-center gap-2">
|
||||
상태
|
||||
{sortField === "is_active" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("updated_date")}>
|
||||
<div className="flex items-center gap-2">
|
||||
최종 수정일
|
||||
{sortField === "updated_date" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="text-center">작업</TableHead>
|
||||
<div className="bg-card shadow-sm">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-background">
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold cursor-pointer hover:bg-muted/50" onClick={() => handleSort("sort_order")}>
|
||||
<div className="flex items-center gap-2">
|
||||
순서
|
||||
{sortField === "sort_order" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold cursor-pointer hover:bg-muted/50" onClick={() => handleSort("web_type")}>
|
||||
<div className="flex items-center gap-2">
|
||||
웹타입 코드
|
||||
{sortField === "web_type" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold cursor-pointer hover:bg-muted/50" onClick={() => handleSort("type_name")}>
|
||||
<div className="flex items-center gap-2">
|
||||
웹타입명
|
||||
{sortField === "type_name" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold cursor-pointer hover:bg-muted/50" onClick={() => handleSort("category")}>
|
||||
<div className="flex items-center gap-2">
|
||||
카테고리
|
||||
{sortField === "category" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">설명</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold cursor-pointer hover:bg-muted/50" onClick={() => handleSort("component_name")}>
|
||||
<div className="flex items-center gap-2">
|
||||
연결된 컴포넌트
|
||||
{sortField === "component_name" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold cursor-pointer hover:bg-muted/50" onClick={() => handleSort("config_panel")}>
|
||||
<div className="flex items-center gap-2">
|
||||
설정 패널
|
||||
{sortField === "config_panel" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold cursor-pointer hover:bg-muted/50" onClick={() => handleSort("is_active")}>
|
||||
<div className="flex items-center gap-2">
|
||||
상태
|
||||
{sortField === "is_active" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold cursor-pointer hover:bg-muted/50" onClick={() => handleSort("updated_date")}>
|
||||
<div className="flex items-center gap-2">
|
||||
최종 수정일
|
||||
{sortField === "updated_date" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold text-center">작업</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
|
|
@ -325,7 +324,7 @@ export default function WebTypesManagePage() {
|
|||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Trash2 className="h-4 w-4 text-red-500" />
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
|
|
@ -341,7 +340,7 @@ export default function WebTypesManagePage() {
|
|||
<AlertDialogAction
|
||||
onClick={() => handleDelete(webType.web_type, webType.type_name)}
|
||||
disabled={isDeleting}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
className="bg-destructive hover:bg-destructive/90"
|
||||
>
|
||||
{isDeleting ? "삭제 중..." : "삭제"}
|
||||
</AlertDialogAction>
|
||||
|
|
@ -355,12 +354,11 @@ export default function WebTypesManagePage() {
|
|||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{deleteError && (
|
||||
<div className="mt-4 rounded-md border border-red-200 bg-red-50 p-4">
|
||||
<p className="text-red-600">
|
||||
<div className="mt-4 rounded-md border border-destructive/30 bg-destructive/10 p-4">
|
||||
<p className="text-destructive">
|
||||
삭제 중 오류가 발생했습니다: {deleteError instanceof Error ? deleteError.message : "알 수 없는 오류"}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -829,7 +829,9 @@ export default function TableManagementPage() {
|
|||
<div
|
||||
key={table.tableName}
|
||||
className={`bg-card rounded-lg p-4 shadow-sm transition-all ${
|
||||
selectedTable === table.tableName ? "shadow-md" : "hover:shadow-md"
|
||||
selectedTable === table.tableName
|
||||
? "shadow-md bg-muted/30"
|
||||
: "hover:shadow-lg hover:bg-muted/20"
|
||||
}`}
|
||||
style={
|
||||
selectedTable === table.tableName
|
||||
|
|
@ -876,7 +878,7 @@ export default function TableManagementPage() {
|
|||
<div className="flex h-full flex-col space-y-4 overflow-hidden">
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{!selectedTable ? (
|
||||
<div className="bg-card flex h-64 flex-col items-center justify-center rounded-lg border shadow-sm">
|
||||
<div className="bg-card flex h-64 flex-col items-center justify-center rounded-lg border">
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{getTextFromUI(TABLE_MANAGEMENT_KEYS.SELECT_TABLE_PLACEHOLDER, "테이블을 선택해주세요")}
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ export function CompanyTable({ companies, isLoading, onEdit, onDelete }: Company
|
|||
return (
|
||||
<>
|
||||
{/* 데스크톱 테이블 뷰 (lg 이상) */}
|
||||
<div className="hidden bg-card shadow-sm lg:block">
|
||||
<div className="hidden bg-card lg:block">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ export const MenuManagement: React.FC = () => {
|
|||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [selectedMenuId, setSelectedMenuId] = useState<string>("");
|
||||
const [selectedMenus, setSelectedMenus] = useState<Set<string>>(new Set());
|
||||
|
||||
// 메뉴 관리 화면용 로컬 상태 (모든 상태의 메뉴 표시)
|
||||
const [localAdminMenus, setLocalAdminMenus] = useState<MenuItem[]>([]);
|
||||
const [localUserMenus, setLocalUserMenus] = useState<MenuItem[]>([]);
|
||||
|
||||
// 다국어 텍스트 훅 사용
|
||||
// getMenuText는 더 이상 사용하지 않음 - getUITextSync만 사용
|
||||
|
|
@ -176,6 +180,7 @@ export const MenuManagement: React.FC = () => {
|
|||
// 초기 로딩
|
||||
useEffect(() => {
|
||||
loadCompanies();
|
||||
loadMenus(false); // 메뉴 목록 로드 (메뉴 관리 화면용 - 모든 상태 표시)
|
||||
// 사용자 언어가 설정되지 않았을 때만 기본 텍스트 설정
|
||||
if (!userLang) {
|
||||
initializeDefaultTexts();
|
||||
|
|
@ -373,12 +378,54 @@ export const MenuManagement: React.FC = () => {
|
|||
};
|
||||
}, [isCompanyDropdownOpen]);
|
||||
|
||||
// 특정 메뉴 타입만 로드하는 함수
|
||||
const loadMenusForType = async (type: MenuType, showLoading = true) => {
|
||||
try {
|
||||
if (showLoading) {
|
||||
setLoading(true);
|
||||
}
|
||||
|
||||
if (type === "admin") {
|
||||
const adminResponse = await menuApi.getAdminMenusForManagement();
|
||||
if (adminResponse.success && adminResponse.data) {
|
||||
setLocalAdminMenus(adminResponse.data);
|
||||
}
|
||||
} else {
|
||||
const userResponse = await menuApi.getUserMenusForManagement();
|
||||
if (userResponse.success && userResponse.data) {
|
||||
setLocalUserMenus(userResponse.data);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(getUITextSync("message.error.load.menu.list"));
|
||||
} finally {
|
||||
if (showLoading) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const loadMenus = async (showLoading = true) => {
|
||||
// console.log(`📋 메뉴 목록 조회 시작 (showLoading: ${showLoading})`);
|
||||
try {
|
||||
if (showLoading) {
|
||||
setLoading(true);
|
||||
}
|
||||
|
||||
// 선택된 메뉴 타입에 해당하는 메뉴만 로드
|
||||
if (selectedMenuType === "admin") {
|
||||
const adminResponse = await menuApi.getAdminMenusForManagement();
|
||||
if (adminResponse.success && adminResponse.data) {
|
||||
setLocalAdminMenus(adminResponse.data);
|
||||
}
|
||||
} else {
|
||||
const userResponse = await menuApi.getUserMenusForManagement();
|
||||
if (userResponse.success && userResponse.data) {
|
||||
setLocalUserMenus(userResponse.data);
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 메뉴 상태도 업데이트 (좌측 사이드바용)
|
||||
await refreshMenus();
|
||||
// console.log("📋 메뉴 목록 조회 성공");
|
||||
} catch (error) {
|
||||
|
|
@ -558,7 +605,7 @@ export const MenuManagement: React.FC = () => {
|
|||
|
||||
const handleAddMenu = (parentId: string, menuType: string, level: number) => {
|
||||
// 상위 메뉴의 회사 정보 찾기
|
||||
const currentMenus = selectedMenuType === "admin" ? adminMenus : userMenus;
|
||||
const currentMenus = selectedMenuType === "admin" ? localAdminMenus : localUserMenus;
|
||||
const parentMenu = currentMenus.find((menu) => menu.objid === parentId);
|
||||
|
||||
setFormData({
|
||||
|
|
@ -575,7 +622,7 @@ export const MenuManagement: React.FC = () => {
|
|||
// console.log("🔧 메뉴 수정 시작 - menuId:", menuId);
|
||||
|
||||
// 현재 메뉴 정보 찾기
|
||||
const currentMenus = selectedMenuType === "admin" ? adminMenus : userMenus;
|
||||
const currentMenus = selectedMenuType === "admin" ? localAdminMenus : localUserMenus;
|
||||
const menuToEdit = currentMenus.find((menu) => (menu.objid || menu.OBJID) === menuId);
|
||||
|
||||
if (menuToEdit) {
|
||||
|
|
@ -614,7 +661,7 @@ export const MenuManagement: React.FC = () => {
|
|||
};
|
||||
|
||||
const handleSelectAllMenus = (checked: boolean) => {
|
||||
const currentMenus = selectedMenuType === "admin" ? adminMenus : userMenus;
|
||||
const currentMenus = selectedMenuType === "admin" ? localAdminMenus : localUserMenus;
|
||||
if (checked) {
|
||||
// 모든 메뉴 선택 (최상위 메뉴 포함)
|
||||
setSelectedMenus(new Set(currentMenus.map((menu) => menu.objid || menu.OBJID || "")));
|
||||
|
|
@ -726,7 +773,8 @@ export const MenuManagement: React.FC = () => {
|
|||
};
|
||||
|
||||
const getCurrentMenus = () => {
|
||||
const currentMenus = selectedMenuType === "admin" ? adminMenus : userMenus;
|
||||
// 메뉴 관리 화면용: 모든 상태의 메뉴 표시 (localAdminMenus/localUserMenus 사용)
|
||||
const currentMenus = selectedMenuType === "admin" ? localAdminMenus : localUserMenus;
|
||||
|
||||
// 검색어 필터링
|
||||
let filteredMenus = currentMenus;
|
||||
|
|
@ -755,6 +803,13 @@ export const MenuManagement: React.FC = () => {
|
|||
setSelectedMenuType(type);
|
||||
setSelectedMenus(new Set()); // 선택된 메뉴 초기화
|
||||
setExpandedMenus(new Set()); // 메뉴 타입 변경 시 확장 상태 초기화
|
||||
|
||||
// 선택한 메뉴 타입에 해당하는 메뉴만 로드
|
||||
if (type === "admin" && localAdminMenus.length === 0) {
|
||||
loadMenusForType("admin", false);
|
||||
} else if (type === "user" && localUserMenus.length === 0) {
|
||||
loadMenusForType("user", false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleExpand = (menuId: string) => {
|
||||
|
|
@ -777,8 +832,8 @@ export const MenuManagement: React.FC = () => {
|
|||
|
||||
// uiTextsCount를 useMemo로 계산하여 상태 변경 시에만 재계산
|
||||
const uiTextsCount = useMemo(() => Object.keys(uiTexts).length, [uiTexts]);
|
||||
const adminMenusCount = useMemo(() => adminMenus?.length || 0, [adminMenus]);
|
||||
const userMenusCount = useMemo(() => userMenus?.length || 0, [userMenus]);
|
||||
const adminMenusCount = useMemo(() => localAdminMenus?.length || 0, [localAdminMenus]);
|
||||
const userMenusCount = useMemo(() => localUserMenus?.length || 0, [localUserMenus]);
|
||||
|
||||
// 디버깅을 위한 간단한 상태 표시
|
||||
// console.log("🔍 MenuManagement 렌더링 상태:", {
|
||||
|
|
@ -823,7 +878,7 @@ export const MenuManagement: React.FC = () => {
|
|||
</p>
|
||||
</div>
|
||||
<Badge variant={selectedMenuType === "admin" ? "default" : "secondary"}>
|
||||
{adminMenus.length}
|
||||
{localAdminMenus.length}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -842,7 +897,7 @@ export const MenuManagement: React.FC = () => {
|
|||
</p>
|
||||
</div>
|
||||
<Badge variant={selectedMenuType === "user" ? "default" : "secondary"}>
|
||||
{userMenus.length}
|
||||
{localUserMenus.length}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -145,10 +145,10 @@ export const MenuTable: React.FC<MenuTableProps> = ({
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
{title && <h3 className="text-lg font-semibold">{title}</h3>}
|
||||
<div className="bg-card shadow-sm">
|
||||
<div className="bg-card">
|
||||
<div className="max-h-[calc(100vh-350px)] overflow-auto">
|
||||
<Table noWrapper>
|
||||
<TableHeader className="sticky top-0 z-20 bg-background shadow-sm">
|
||||
<TableHeader className="sticky top-0 z-20 bg-background">
|
||||
<TableRow>
|
||||
<TableHead className="h-12 w-12 text-sm font-semibold">
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -252,34 +252,34 @@ export function RestApiConnectionList() {
|
|||
|
||||
{/* 연결 목록 */}
|
||||
{loading ? (
|
||||
<div className="bg-card flex h-64 items-center justify-center shadow-sm">
|
||||
<div className="text-muted-foreground text-sm">로딩 중...</div>
|
||||
<div className="flex h-64 items-center justify-center bg-card">
|
||||
<div className="text-sm text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
) : connections.length === 0 ? (
|
||||
<div className="bg-card flex h-64 flex-col items-center justify-center shadow-sm">
|
||||
<div className="flex h-64 flex-col items-center justify-center bg-card">
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<p className="text-muted-foreground text-sm">등록된 REST API 연결이 없습니다</p>
|
||||
<p className="text-sm text-muted-foreground">등록된 REST API 연결이 없습니다</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-card shadow-sm">
|
||||
<div className="bg-card">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="h-12 text-sm font-semibold">연결명</TableHead>
|
||||
<TableHead className="h-12 text-sm font-semibold">기본 URL</TableHead>
|
||||
<TableHead className="h-12 text-sm font-semibold">인증 타입</TableHead>
|
||||
<TableHead className="h-12 text-sm font-semibold">헤더 수</TableHead>
|
||||
<TableHead className="h-12 text-sm font-semibold">상태</TableHead>
|
||||
<TableHead className="h-12 text-sm font-semibold">마지막 테스트</TableHead>
|
||||
<TableHead className="h-12 text-sm font-semibold">연결 테스트</TableHead>
|
||||
<TableHead className="h-12 text-right text-sm font-semibold">작업</TableHead>
|
||||
<TableRow className="bg-background">
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">연결명</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">기본 URL</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">인증 타입</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">헤더 수</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">상태</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">마지막 테스트</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">연결 테스트</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-right text-sm font-semibold">작업</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{connections.map((connection) => (
|
||||
<TableRow key={connection.id} className="hover:bg-muted/50 transition-colors">
|
||||
<TableCell className="h-16 text-sm">
|
||||
<TableRow key={connection.id} className="bg-background transition-colors hover:bg-muted/50">
|
||||
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||
<div className="max-w-[200px]">
|
||||
<div className="truncate font-medium" title={connection.connection_name}>
|
||||
{connection.connection_name}
|
||||
|
|
@ -291,23 +291,23 @@ export function RestApiConnectionList() {
|
|||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="h-16 font-mono text-sm">
|
||||
<TableCell className="h-16 px-6 py-3 font-mono text-sm">
|
||||
<div className="max-w-[300px] truncate" title={connection.base_url}>
|
||||
{connection.base_url}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="h-16 text-sm">
|
||||
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||
<Badge variant="outline">{AUTH_TYPE_LABELS[connection.auth_type] || connection.auth_type}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="h-16 text-center text-sm">
|
||||
<TableCell className="h-16 px-6 py-3 text-center text-sm">
|
||||
{Object.keys(connection.default_headers || {}).length}
|
||||
</TableCell>
|
||||
<TableCell className="h-16 text-sm">
|
||||
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||
<Badge variant={connection.is_active === "Y" ? "default" : "secondary"}>
|
||||
{connection.is_active === "Y" ? "활성" : "비활성"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="h-16 text-sm">
|
||||
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||
{connection.last_test_date ? (
|
||||
<div>
|
||||
<div>{new Date(connection.last_test_date).toLocaleDateString()}</div>
|
||||
|
|
@ -322,7 +322,7 @@ export function RestApiConnectionList() {
|
|||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="h-16 text-sm">
|
||||
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
@ -340,7 +340,7 @@ export function RestApiConnectionList() {
|
|||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="h-16 text-right">
|
||||
<TableCell className="h-16 px-6 py-3 text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export function UserAuthTable({ users, isLoading, paginationInfo, onEditAuth, on
|
|||
// 로딩 스켈레톤
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="bg-card hidden shadow-sm lg:block">
|
||||
<div className="bg-card hidden lg:block">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
|
|
@ -123,7 +123,7 @@ export function UserAuthTable({ users, isLoading, paginationInfo, onEditAuth, on
|
|||
// 빈 상태
|
||||
if (users.length === 0) {
|
||||
return (
|
||||
<div className="bg-card flex h-64 flex-col items-center justify-center shadow-sm">
|
||||
<div className="bg-card flex h-64 flex-col items-center justify-center">
|
||||
<p className="text-muted-foreground text-sm">등록된 사용자가 없습니다.</p>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -133,7 +133,7 @@ export function UserAuthTable({ users, isLoading, paginationInfo, onEditAuth, on
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 데스크톱 테이블 */}
|
||||
<div className="bg-card hidden shadow-sm lg:block">
|
||||
<div className="bg-card hidden lg:block">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
|
|
@ -182,7 +182,7 @@ export function UserAuthTable({ users, isLoading, paginationInfo, onEditAuth, on
|
|||
return (
|
||||
<div
|
||||
key={user.userId}
|
||||
className="bg-card hover:bg-muted/50 rounded-lg border p-4 shadow-sm transition-colors"
|
||||
className="bg-card hover:bg-muted/50 rounded-lg border p-4 transition-colors"
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<div className="mb-4 flex items-start justify-between">
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ export function UserTable({
|
|||
return (
|
||||
<>
|
||||
{/* 데스크톱 테이블 뷰 (lg 이상) */}
|
||||
<div className="bg-card hidden shadow-sm lg:block">
|
||||
<div className="bg-card hidden lg:block">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
|
|
|
|||
|
|
@ -146,12 +146,6 @@ export default function DataFlowList({ onLoadFlow }: DataFlowListProps) {
|
|||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 섹션 제목 */}
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-xl font-semibold">플로우 목록</h2>
|
||||
<p className="text-sm text-muted-foreground">저장된 노드 플로우를 불러오거나 새로운 플로우를 생성합니다</p>
|
||||
</div>
|
||||
|
||||
{/* 검색 및 액션 영역 */}
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
{/* 검색 영역 */}
|
||||
|
|
@ -184,33 +178,33 @@ export default function DataFlowList({ onLoadFlow }: DataFlowListProps) {
|
|||
{loading ? (
|
||||
<>
|
||||
{/* 데스크톱 테이블 스켈레톤 */}
|
||||
<div className="hidden rounded-lg border bg-card shadow-sm lg:block">
|
||||
<div className="hidden bg-card shadow-sm lg:block">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-b bg-muted/50 hover:bg-muted/50">
|
||||
<TableHead className="h-12 text-sm font-semibold">플로우명</TableHead>
|
||||
<TableHead className="h-12 text-sm font-semibold">설명</TableHead>
|
||||
<TableHead className="h-12 text-sm font-semibold">생성일</TableHead>
|
||||
<TableHead className="h-12 text-sm font-semibold">최근 수정</TableHead>
|
||||
<TableHead className="h-12 text-right text-sm font-semibold">작업</TableHead>
|
||||
<TableRow className="bg-background">
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">플로우명</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">설명</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">생성일</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">최근 수정</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-right text-sm font-semibold">작업</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<TableRow key={index} className="border-b">
|
||||
<TableCell className="h-16">
|
||||
<TableRow key={index} className="bg-background">
|
||||
<TableCell className="h-16 px-6 py-3">
|
||||
<div className="h-4 w-32 animate-pulse rounded bg-muted"></div>
|
||||
</TableCell>
|
||||
<TableCell className="h-16">
|
||||
<TableCell className="h-16 px-6 py-3">
|
||||
<div className="h-4 w-48 animate-pulse rounded bg-muted"></div>
|
||||
</TableCell>
|
||||
<TableCell className="h-16">
|
||||
<TableCell className="h-16 px-6 py-3">
|
||||
<div className="h-4 w-24 animate-pulse rounded bg-muted"></div>
|
||||
</TableCell>
|
||||
<TableCell className="h-16">
|
||||
<TableCell className="h-16 px-6 py-3">
|
||||
<div className="h-4 w-24 animate-pulse rounded bg-muted"></div>
|
||||
</TableCell>
|
||||
<TableCell className="h-16">
|
||||
<TableCell className="h-16 px-6 py-3">
|
||||
<div className="flex justify-end">
|
||||
<div className="h-8 w-8 animate-pulse rounded bg-muted"></div>
|
||||
</div>
|
||||
|
|
@ -264,46 +258,46 @@ export default function DataFlowList({ onLoadFlow }: DataFlowListProps) {
|
|||
) : (
|
||||
<>
|
||||
{/* 데스크톱 테이블 뷰 (lg 이상) */}
|
||||
<div className="hidden rounded-lg border bg-card shadow-sm lg:block">
|
||||
<div className="hidden bg-card shadow-sm lg:block">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-b bg-muted/50 hover:bg-muted/50">
|
||||
<TableHead className="h-12 text-sm font-semibold">플로우명</TableHead>
|
||||
<TableHead className="h-12 text-sm font-semibold">설명</TableHead>
|
||||
<TableHead className="h-12 text-sm font-semibold">생성일</TableHead>
|
||||
<TableHead className="h-12 text-sm font-semibold">최근 수정</TableHead>
|
||||
<TableHead className="h-12 text-right text-sm font-semibold">작업</TableHead>
|
||||
<TableRow className="bg-background">
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">플로우명</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">설명</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">생성일</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">최근 수정</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-right text-sm font-semibold">작업</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredFlows.map((flow) => (
|
||||
<TableRow
|
||||
key={flow.flowId}
|
||||
className="cursor-pointer border-b transition-colors hover:bg-muted/50"
|
||||
className="bg-background transition-colors hover:bg-muted/50 cursor-pointer"
|
||||
onClick={() => onLoadFlow(flow.flowId)}
|
||||
>
|
||||
<TableCell className="h-16 text-sm">
|
||||
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||
<div className="flex items-center font-medium">
|
||||
<Network className="mr-2 h-4 w-4 text-primary" />
|
||||
{flow.flowName}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="h-16 text-sm">
|
||||
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||
<div className="text-muted-foreground">{flow.flowDescription || "설명 없음"}</div>
|
||||
</TableCell>
|
||||
<TableCell className="h-16 text-sm">
|
||||
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||
<div className="flex items-center text-muted-foreground">
|
||||
<Calendar className="mr-1 h-3 w-3" />
|
||||
{new Date(flow.createdAt).toLocaleDateString()}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="h-16 text-sm">
|
||||
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||
<div className="flex items-center text-muted-foreground">
|
||||
<Calendar className="mr-1 h-3 w-3" />
|
||||
{new Date(flow.updatedAt).toLocaleDateString()}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="h-16" onClick={(e) => e.stopPropagation()}>
|
||||
<TableCell className="h-16 px-6 py-3" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ function TabsList({
|
|||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||
"bg-muted/30 text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -42,7 +42,7 @@ function TabsTrigger({
|
|||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ export function MenuProvider({ children }: { children: ReactNode }) {
|
|||
}
|
||||
|
||||
// 관리자 메뉴와 사용자 메뉴를 병렬로 로드
|
||||
// 좌측 사이드바용: active만 표시
|
||||
const [adminResponse, userResponse] = await Promise.all([menuApi.getAdminMenus(), menuApi.getUserMenus()]);
|
||||
|
||||
if (adminResponse.success && adminResponse.data) {
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export interface ApiResponse<T> {
|
|||
}
|
||||
|
||||
export const menuApi = {
|
||||
// 관리자 메뉴 목록 조회
|
||||
// 관리자 메뉴 목록 조회 (좌측 사이드바용 - active만 표시)
|
||||
getAdminMenus: async (): Promise<ApiResponse<MenuItem[]>> => {
|
||||
const response = await apiClient.get("/admin/menus", { params: { menuType: "0" } });
|
||||
if (response.data.success && response.data.data && response.data.data.length > 0) {
|
||||
|
|
@ -84,12 +84,24 @@ export const menuApi = {
|
|||
return response.data;
|
||||
},
|
||||
|
||||
// 사용자 메뉴 목록 조회
|
||||
// 사용자 메뉴 목록 조회 (좌측 사이드바용 - active만 표시)
|
||||
getUserMenus: async (): Promise<ApiResponse<MenuItem[]>> => {
|
||||
const response = await apiClient.get("/admin/menus", { params: { menuType: "1" } });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// 관리자 메뉴 목록 조회 (메뉴 관리 화면용 - 모든 상태 표시)
|
||||
getAdminMenusForManagement: async (): Promise<ApiResponse<MenuItem[]>> => {
|
||||
const response = await apiClient.get("/admin/menus", { params: { menuType: "0", includeInactive: "true" } });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// 사용자 메뉴 목록 조회 (메뉴 관리 화면용 - 모든 상태 표시)
|
||||
getUserMenusForManagement: async (): Promise<ApiResponse<MenuItem[]>> => {
|
||||
const response = await apiClient.get("/admin/menus", { params: { menuType: "1", includeInactive: "true" } });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// 메뉴 정보 조회
|
||||
getMenuInfo: async (menuId: string): Promise<ApiResponse<MenuItem>> => {
|
||||
const response = await apiClient.get(`/admin/menus/${menuId}`);
|
||||
|
|
|
|||
Loading…
Reference in New Issue