Merge pull request 'feature/screen-management' (#171) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/171
This commit is contained in:
commit
224e9a2522
|
|
@ -25,19 +25,22 @@ export async function getAdminMenus(
|
||||||
const userType = req.user?.userType;
|
const userType = req.user?.userType;
|
||||||
const userLang = (req.query.userLang as string) || "ko";
|
const userLang = (req.query.userLang as string) || "ko";
|
||||||
const menuType = req.query.menuType as string | undefined; // menuType 파라미터 추가
|
const menuType = req.query.menuType as string | undefined; // menuType 파라미터 추가
|
||||||
|
const includeInactive = req.query.includeInactive === "true"; // includeInactive 파라미터 추가
|
||||||
|
|
||||||
logger.info(`사용자 ID: ${userId}`);
|
logger.info(`사용자 ID: ${userId}`);
|
||||||
logger.info(`사용자 회사 코드: ${userCompanyCode}`);
|
logger.info(`사용자 회사 코드: ${userCompanyCode}`);
|
||||||
logger.info(`사용자 유형: ${userType}`);
|
logger.info(`사용자 유형: ${userType}`);
|
||||||
logger.info(`사용자 로케일: ${userLang}`);
|
logger.info(`사용자 로케일: ${userLang}`);
|
||||||
logger.info(`메뉴 타입: ${menuType || "전체"}`);
|
logger.info(`메뉴 타입: ${menuType || "전체"}`);
|
||||||
|
logger.info(`비활성 메뉴 포함: ${includeInactive}`);
|
||||||
|
|
||||||
const paramMap = {
|
const paramMap = {
|
||||||
userId,
|
userId,
|
||||||
userCompanyCode,
|
userCompanyCode,
|
||||||
userType,
|
userType,
|
||||||
userLang,
|
userLang,
|
||||||
menuType, // menuType 추가
|
menuType, // includeInactive와 관계없이 menuType 유지 (관리자/사용자 구분)
|
||||||
|
includeInactive, // includeInactive 추가
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuList = await AdminService.getAdminMenuList(paramMap);
|
const menuList = await AdminService.getAdminMenuList(paramMap);
|
||||||
|
|
@ -1081,9 +1084,41 @@ export async function saveMenu(
|
||||||
return;
|
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를 사용한 메뉴 저장
|
// Raw Query를 사용한 메뉴 저장
|
||||||
const objid = Date.now(); // 고유 ID 생성
|
const objid = Date.now(); // 고유 ID 생성
|
||||||
const companyCode = req.user.companyCode;
|
const companyCode = requestCompanyCode || userCompanyCode;
|
||||||
|
|
||||||
const [savedMenu] = await query<any>(
|
const [savedMenu] = await query<any>(
|
||||||
`INSERT INTO menu_info (
|
`INSERT INTO menu_info (
|
||||||
|
|
@ -1164,7 +1199,73 @@ export async function updateMenu(
|
||||||
return;
|
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를 사용한 메뉴 수정
|
// Raw Query를 사용한 메뉴 수정
|
||||||
const [updatedMenu] = await query<any>(
|
const [updatedMenu] = await query<any>(
|
||||||
|
|
@ -1239,6 +1340,56 @@ export async function deleteMenu(
|
||||||
const { menuId } = req.params;
|
const { menuId } = req.params;
|
||||||
logger.info(`메뉴 삭제 요청: menuId = ${menuId}`, { user: req.user });
|
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를 사용한 메뉴 삭제
|
// Raw Query를 사용한 메뉴 삭제
|
||||||
const [deletedMenu] = await query<any>(
|
const [deletedMenu] = await query<any>(
|
||||||
`DELETE FROM menu_info WHERE objid = $1 RETURNING *`,
|
`DELETE FROM menu_info WHERE objid = $1 RETURNING *`,
|
||||||
|
|
@ -1292,6 +1443,51 @@ export async function deleteMenusBatch(
|
||||||
return;
|
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를 사용한 메뉴 일괄 삭제
|
// Raw Query를 사용한 메뉴 일괄 삭제
|
||||||
let deletedCount = 0;
|
let deletedCount = 0;
|
||||||
let failedCount = 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, // 메뉴 수정
|
updateMenu, // 메뉴 수정
|
||||||
deleteMenu, // 메뉴 삭제
|
deleteMenu, // 메뉴 삭제
|
||||||
deleteMenusBatch, // 메뉴 일괄 삭제
|
deleteMenusBatch, // 메뉴 일괄 삭제
|
||||||
|
toggleMenuStatus, // 메뉴 상태 토글
|
||||||
getUserList,
|
getUserList,
|
||||||
getUserInfo, // 사용자 상세 조회
|
getUserInfo, // 사용자 상세 조회
|
||||||
getUserHistory, // 사용자 변경이력 조회
|
getUserHistory, // 사용자 변경이력 조회
|
||||||
|
|
@ -37,6 +38,7 @@ router.get("/user-menus", getUserMenus);
|
||||||
router.get("/menus/:menuId", getMenuInfo);
|
router.get("/menus/:menuId", getMenuInfo);
|
||||||
router.post("/menus", saveMenu); // 메뉴 추가
|
router.post("/menus", saveMenu); // 메뉴 추가
|
||||||
router.put("/menus/:menuId", updateMenu); // 메뉴 수정
|
router.put("/menus/:menuId", updateMenu); // 메뉴 수정
|
||||||
|
router.put("/menus/:menuId/toggle", toggleMenuStatus); // 메뉴 상태 토글
|
||||||
router.delete("/menus/batch", deleteMenusBatch); // 메뉴 일괄 삭제 (순서 중요!)
|
router.delete("/menus/batch", deleteMenusBatch); // 메뉴 일괄 삭제 (순서 중요!)
|
||||||
router.delete("/menus/:menuId", deleteMenu); // 메뉴 삭제
|
router.delete("/menus/:menuId", deleteMenu); // 메뉴 삭제
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,15 @@ export class AdminService {
|
||||||
|
|
||||||
// menuType에 따른 WHERE 조건 생성
|
// menuType에 따른 WHERE 조건 생성
|
||||||
const menuTypeCondition =
|
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. 권한 그룹 기반 필터링 (좌측 사이드바인 경우만)
|
// 1. 권한 그룹 기반 필터링 (좌측 사이드바인 경우만)
|
||||||
let authFilter = "";
|
let authFilter = "";
|
||||||
|
|
@ -27,8 +35,8 @@ export class AdminService {
|
||||||
let queryParams: any[] = [userLang];
|
let queryParams: any[] = [userLang];
|
||||||
let paramIndex = 2;
|
let paramIndex = 2;
|
||||||
|
|
||||||
if (menuType !== undefined && userType !== "SUPER_ADMIN") {
|
if (menuType !== undefined && userType !== "SUPER_ADMIN" && !isManagementScreen) {
|
||||||
// 좌측 사이드바 + SUPER_ADMIN이 아닌 경우
|
// 좌측 사이드바 + SUPER_ADMIN이 아닌 경우만 권한 체크
|
||||||
const userRoleGroups = await query<any>(
|
const userRoleGroups = await query<any>(
|
||||||
`
|
`
|
||||||
SELECT DISTINCT am.objid AS role_objid, am.auth_name
|
SELECT DISTINCT am.objid AS role_objid, am.auth_name
|
||||||
|
|
@ -123,7 +131,7 @@ export class AdminService {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (menuType !== undefined && userType === "SUPER_ADMIN") {
|
} else if (menuType !== undefined && userType === "SUPER_ADMIN" && !isManagementScreen) {
|
||||||
// 좌측 사이드바 + SUPER_ADMIN: 권한 그룹 체크 없이 모든 공통 메뉴 표시
|
// 좌측 사이드바 + SUPER_ADMIN: 권한 그룹 체크 없이 모든 공통 메뉴 표시
|
||||||
logger.info(`✅ 최고 관리자는 권한 그룹 체크 없이 모든 공통 메뉴 표시`);
|
logger.info(`✅ 최고 관리자는 권한 그룹 체크 없이 모든 공통 메뉴 표시`);
|
||||||
// unionFilter는 비워둠 (하위 메뉴도 공통 메뉴만)
|
// unionFilter는 비워둠 (하위 메뉴도 공통 메뉴만)
|
||||||
|
|
@ -136,7 +144,7 @@ export class AdminService {
|
||||||
// SUPER_ADMIN과 COMPANY_ADMIN 구분
|
// SUPER_ADMIN과 COMPANY_ADMIN 구분
|
||||||
if (userType === "SUPER_ADMIN" && userCompanyCode === "*") {
|
if (userType === "SUPER_ADMIN" && userCompanyCode === "*") {
|
||||||
// SUPER_ADMIN
|
// SUPER_ADMIN
|
||||||
if (menuType === undefined) {
|
if (isManagementScreen) {
|
||||||
// 메뉴 관리 화면: 모든 메뉴
|
// 메뉴 관리 화면: 모든 메뉴
|
||||||
logger.info("✅ 메뉴 관리 화면 (SUPER_ADMIN): 모든 메뉴 표시");
|
logger.info("✅ 메뉴 관리 화면 (SUPER_ADMIN): 모든 메뉴 표시");
|
||||||
companyFilter = "";
|
companyFilter = "";
|
||||||
|
|
@ -145,16 +153,34 @@ export class AdminService {
|
||||||
logger.info("✅ 좌측 사이드바 (SUPER_ADMIN): 공통 메뉴만 표시");
|
logger.info("✅ 좌측 사이드바 (SUPER_ADMIN): 공통 메뉴만 표시");
|
||||||
companyFilter = `AND MENU.COMPANY_CODE = '*'`;
|
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(
|
logger.info(
|
||||||
`✅ 메뉴 관리 화면: 회사 ${userCompanyCode} + 공통 메뉴 표시`
|
`✅ 좌측 사이드바: 회사 ${userCompanyCode} 메뉴 표시 (${userType})`
|
||||||
);
|
);
|
||||||
companyFilter = `AND (MENU.COMPANY_CODE = $${paramIndex} OR MENU.COMPANY_CODE = '*')`;
|
// companyFilter는 authFilter에서 이미 처리됨
|
||||||
queryParams.push(userCompanyCode);
|
|
||||||
paramIndex++;
|
|
||||||
}
|
}
|
||||||
// menuType이 정의된 경우 (좌측 사이드바)는 authFilter에서 이미 회사 필터링 포함
|
|
||||||
|
|
||||||
// 기존 Java의 selectAdminMenuList 쿼리를 Raw Query로 포팅
|
// 기존 Java의 selectAdminMenuList 쿼리를 Raw Query로 포팅
|
||||||
// WITH RECURSIVE 쿼리 구현
|
// WITH RECURSIVE 쿼리 구현
|
||||||
|
|
@ -237,7 +263,7 @@ export class AdminService {
|
||||||
)
|
)
|
||||||
FROM MENU_INFO MENU
|
FROM MENU_INFO MENU
|
||||||
WHERE ${menuTypeCondition}
|
WHERE ${menuTypeCondition}
|
||||||
AND STATUS = 'active'
|
AND ${statusCondition}
|
||||||
${companyFilter}
|
${companyFilter}
|
||||||
${authFilter}
|
${authFilter}
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
|
|
@ -304,7 +330,7 @@ export class AdminService {
|
||||||
FROM MENU_INFO MENU_SUB
|
FROM MENU_INFO MENU_SUB
|
||||||
JOIN V_MENU ON MENU_SUB.PARENT_OBJ_ID = V_MENU.OBJID
|
JOIN V_MENU ON MENU_SUB.PARENT_OBJ_ID = V_MENU.OBJID
|
||||||
WHERE MENU_SUB.OBJID != ANY(V_MENU.PATH)
|
WHERE MENU_SUB.OBJID != ANY(V_MENU.PATH)
|
||||||
AND MENU_SUB.STATUS = 'active'
|
AND ${subStatusCondition}
|
||||||
${unionFilter}
|
${unionFilter}
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
|
|
|
||||||
|
|
@ -353,14 +353,14 @@ export default function BatchManagementPage() {
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>작업명</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">작업명</TableHead>
|
||||||
<TableHead>타입</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">타입</TableHead>
|
||||||
<TableHead>스케줄</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">스케줄</TableHead>
|
||||||
<TableHead>상태</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">상태</TableHead>
|
||||||
<TableHead>실행 통계</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">실행 통계</TableHead>
|
||||||
<TableHead>성공률</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">성공률</TableHead>
|
||||||
<TableHead>마지막 실행</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">마지막 실행</TableHead>
|
||||||
<TableHead>작업</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">작업</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|
|
||||||
|
|
@ -249,20 +249,20 @@ export default function CollectionManagementPage() {
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>설정명</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">설정명</TableHead>
|
||||||
<TableHead>수집 타입</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">수집 타입</TableHead>
|
||||||
<TableHead>소스 테이블</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">소스 테이블</TableHead>
|
||||||
<TableHead>대상 테이블</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">대상 테이블</TableHead>
|
||||||
<TableHead>스케줄</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">스케줄</TableHead>
|
||||||
<TableHead>상태</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">상태</TableHead>
|
||||||
<TableHead>마지막 수집</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">마지막 수집</TableHead>
|
||||||
<TableHead>작업</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">작업</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{filteredConfigs.map((config) => (
|
{filteredConfigs.map((config) => (
|
||||||
<TableRow key={config.id}>
|
<TableRow key={config.id} className="bg-background transition-colors hover:bg-muted/50">
|
||||||
<TableCell>
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">{config.config_name}</div>
|
<div className="font-medium">{config.config_name}</div>
|
||||||
{config.description && (
|
{config.description && (
|
||||||
|
|
@ -272,27 +272,27 @@ export default function CollectionManagementPage() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
{getTypeBadge(config.collection_type)}
|
{getTypeBadge(config.collection_type)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-mono text-sm">
|
<TableCell className="h-16 px-6 py-3 font-mono text-sm">
|
||||||
{config.source_table}
|
{config.source_table}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-mono text-sm">
|
<TableCell className="h-16 px-6 py-3 font-mono text-sm">
|
||||||
{config.target_table || "-"}
|
{config.target_table || "-"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-mono text-sm">
|
<TableCell className="h-16 px-6 py-3 font-mono text-sm">
|
||||||
{config.schedule_cron || "-"}
|
{config.schedule_cron || "-"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
{getStatusBadge(config.is_active)}
|
{getStatusBadge(config.is_active)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
{config.last_collected_at
|
{config.last_collected_at
|
||||||
? new Date(config.last_collected_at).toLocaleString()
|
? new Date(config.last_collected_at).toLocaleString()
|
||||||
: "-"}
|
: "-"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
|
|
|
||||||
|
|
@ -302,56 +302,56 @@ export default function ExternalConnectionsPage() {
|
||||||
|
|
||||||
{/* 연결 목록 */}
|
{/* 연결 목록 */}
|
||||||
{loading ? (
|
{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 className="text-sm text-muted-foreground">로딩 중...</div>
|
||||||
</div>
|
</div>
|
||||||
) : connections.length === 0 ? (
|
) : 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">
|
<div className="flex flex-col items-center gap-2 text-center">
|
||||||
<p className="text-sm text-muted-foreground">등록된 연결이 없습니다</p>
|
<p className="text-sm text-muted-foreground">등록된 연결이 없습니다</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-lg border bg-card shadow-sm">
|
<div className="bg-card">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="border-b bg-muted/50 hover:bg-muted/50">
|
<TableRow className="bg-background">
|
||||||
<TableHead className="h-12 text-sm font-semibold">연결명</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">연결명</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">DB 타입</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">DB 타입</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">호스트:포트</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">호스트:포트</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">데이터베이스</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">데이터베이스</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">사용자</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">사용자</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">상태</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">상태</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">생성일</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">생성일</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">연결 테스트</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">연결 테스트</TableHead>
|
||||||
<TableHead className="h-12 text-right text-sm font-semibold">작업</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-right text-sm font-semibold">작업</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{connections.map((connection) => (
|
{connections.map((connection) => (
|
||||||
<TableRow key={connection.id} className="border-b transition-colors hover:bg-muted/50">
|
<TableRow key={connection.id} className="bg-background transition-colors hover:bg-muted/50">
|
||||||
<TableCell className="h-16 text-sm">
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
<div className="font-medium">{connection.connection_name}</div>
|
<div className="font-medium">{connection.connection_name}</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="h-16 text-sm">
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
<Badge variant="outline">
|
<Badge variant="outline">
|
||||||
{DB_TYPE_LABELS[connection.db_type] || connection.db_type}
|
{DB_TYPE_LABELS[connection.db_type] || connection.db_type}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="h-16 font-mono text-sm">
|
<TableCell className="h-16 px-6 py-3 font-mono text-sm">
|
||||||
{connection.host}:{connection.port}
|
{connection.host}:{connection.port}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="h-16 font-mono text-sm">{connection.database_name}</TableCell>
|
<TableCell className="h-16 px-6 py-3 font-mono text-sm">{connection.database_name}</TableCell>
|
||||||
<TableCell className="h-16 font-mono text-sm">{connection.username}</TableCell>
|
<TableCell className="h-16 px-6 py-3 font-mono text-sm">{connection.username}</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"}>
|
<Badge variant={connection.is_active === "Y" ? "default" : "secondary"}>
|
||||||
{connection.is_active === "Y" ? "활성" : "비활성"}
|
{connection.is_active === "Y" ? "활성" : "비활성"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="h-16 text-sm">
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
{connection.created_date ? new Date(connection.created_date).toLocaleDateString() : "N/A"}
|
{connection.created_date ? new Date(connection.created_date).toLocaleDateString() : "N/A"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="h-16 text-sm">
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
@ -369,7 +369,7 @@ export default function ExternalConnectionsPage() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="h-16 text-right">
|
<TableCell className="h-16 px-6 py-3 text-right">
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ export default function WebTypesManagePage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-96 items-center justify-center">
|
<div className="flex h-96 items-center justify-center">
|
||||||
<div className="text-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 onClick={() => refetch()} variant="outline">
|
||||||
다시 시도
|
다시 시도
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -127,13 +127,13 @@ export default function WebTypesManagePage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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="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>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900">웹타입 관리</h1>
|
<h1 className="text-3xl font-bold text-foreground">웹타입 관리</h1>
|
||||||
<p className="mt-2 text-gray-600">화면관리에서 사용할 웹타입들을 관리합니다</p>
|
<p className="mt-2 text-muted-foreground">화면관리에서 사용할 웹타입들을 관리합니다</p>
|
||||||
</div>
|
</div>
|
||||||
<Link href="/admin/standards/new">
|
<Link href="/admin/standards/new">
|
||||||
<Button className="shadow-sm">
|
<Button className="shadow-sm">
|
||||||
|
|
@ -144,9 +144,9 @@ export default function WebTypesManagePage() {
|
||||||
|
|
||||||
{/* 필터 및 검색 */}
|
{/* 필터 및 검색 */}
|
||||||
<Card className="shadow-sm">
|
<Card className="shadow-sm">
|
||||||
<CardHeader className="bg-gray-50/50">
|
<CardHeader className="bg-muted/50">
|
||||||
<CardTitle className="flex items-center gap-2 text-lg">
|
<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>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
@ -154,7 +154,7 @@ export default function WebTypesManagePage() {
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
{/* 검색 */}
|
{/* 검색 */}
|
||||||
<div className="relative">
|
<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
|
<Input
|
||||||
placeholder="웹타입명, 설명 검색..."
|
placeholder="웹타입명, 설명 검색..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
|
|
@ -200,74 +200,73 @@ export default function WebTypesManagePage() {
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 결과 통계 */}
|
{/* 결과 통계 */}
|
||||||
<div className="bg-white rounded-lg border px-4 py-3">
|
<div className="bg-background rounded-lg border px-4 py-3">
|
||||||
<p className="text-gray-700 text-sm font-medium">총 {filteredAndSortedWebTypes.length}개의 웹타입이 있습니다.</p>
|
<p className="text-foreground text-sm font-medium">총 {filteredAndSortedWebTypes.length}개의 웹타입이 있습니다.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 웹타입 목록 테이블 */}
|
{/* 웹타입 목록 테이블 */}
|
||||||
<Card className="shadow-sm">
|
<div className="bg-card shadow-sm">
|
||||||
<CardContent className="p-0">
|
<Table>
|
||||||
<Table>
|
<TableHeader>
|
||||||
<TableHeader>
|
<TableRow className="bg-background">
|
||||||
<TableRow>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold cursor-pointer hover:bg-muted/50" onClick={() => handleSort("sort_order")}>
|
||||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("sort_order")}>
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
순서
|
||||||
순서
|
{sortField === "sort_order" &&
|
||||||
{sortField === "sort_order" &&
|
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
</div>
|
||||||
</div>
|
</TableHead>
|
||||||
</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold cursor-pointer hover:bg-muted/50" onClick={() => handleSort("web_type")}>
|
||||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("web_type")}>
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
웹타입 코드
|
||||||
웹타입 코드
|
{sortField === "web_type" &&
|
||||||
{sortField === "web_type" &&
|
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
</div>
|
||||||
</div>
|
</TableHead>
|
||||||
</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold cursor-pointer hover:bg-muted/50" onClick={() => handleSort("type_name")}>
|
||||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("type_name")}>
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
웹타입명
|
||||||
웹타입명
|
{sortField === "type_name" &&
|
||||||
{sortField === "type_name" &&
|
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
</div>
|
||||||
</div>
|
</TableHead>
|
||||||
</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold cursor-pointer hover:bg-muted/50" onClick={() => handleSort("category")}>
|
||||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("category")}>
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
카테고리
|
||||||
카테고리
|
{sortField === "category" &&
|
||||||
{sortField === "category" &&
|
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
</div>
|
||||||
</div>
|
</TableHead>
|
||||||
</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">설명</TableHead>
|
||||||
<TableHead>설명</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold cursor-pointer hover:bg-muted/50" onClick={() => handleSort("component_name")}>
|
||||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("component_name")}>
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
연결된 컴포넌트
|
||||||
연결된 컴포넌트
|
{sortField === "component_name" &&
|
||||||
{sortField === "component_name" &&
|
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
</div>
|
||||||
</div>
|
</TableHead>
|
||||||
</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold cursor-pointer hover:bg-muted/50" onClick={() => handleSort("config_panel")}>
|
||||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("config_panel")}>
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
설정 패널
|
||||||
설정 패널
|
{sortField === "config_panel" &&
|
||||||
{sortField === "config_panel" &&
|
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
</div>
|
||||||
</div>
|
</TableHead>
|
||||||
</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold cursor-pointer hover:bg-muted/50" onClick={() => handleSort("is_active")}>
|
||||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("is_active")}>
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
상태
|
||||||
상태
|
{sortField === "is_active" &&
|
||||||
{sortField === "is_active" &&
|
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
</div>
|
||||||
</div>
|
</TableHead>
|
||||||
</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold cursor-pointer hover:bg-muted/50" onClick={() => handleSort("updated_date")}>
|
||||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("updated_date")}>
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
최종 수정일
|
||||||
최종 수정일
|
{sortField === "updated_date" &&
|
||||||
{sortField === "updated_date" &&
|
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
</div>
|
||||||
</div>
|
</TableHead>
|
||||||
</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold text-center">작업</TableHead>
|
||||||
<TableHead className="text-center">작업</TableHead>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|
@ -279,38 +278,38 @@ export default function WebTypesManagePage() {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
filteredAndSortedWebTypes.map((webType) => (
|
filteredAndSortedWebTypes.map((webType) => (
|
||||||
<TableRow key={webType.web_type}>
|
<TableRow key={webType.web_type} className="bg-background transition-colors hover:bg-muted/50">
|
||||||
<TableCell className="font-mono">{webType.sort_order || 0}</TableCell>
|
<TableCell className="h-16 px-6 py-3 font-mono text-sm">{webType.sort_order || 0}</TableCell>
|
||||||
<TableCell className="font-mono">{webType.web_type}</TableCell>
|
<TableCell className="h-16 px-6 py-3 font-mono text-sm">{webType.web_type}</TableCell>
|
||||||
<TableCell className="font-medium">
|
<TableCell className="h-16 px-6 py-3 font-medium text-sm">
|
||||||
{webType.type_name}
|
{webType.type_name}
|
||||||
{webType.type_name_eng && (
|
{webType.type_name_eng && (
|
||||||
<div className="text-muted-foreground text-xs">{webType.type_name_eng}</div>
|
<div className="text-muted-foreground text-xs">{webType.type_name_eng}</div>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
<Badge variant="secondary">{webType.category}</Badge>
|
<Badge variant="secondary">{webType.category}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="max-w-xs truncate">{webType.description || "-"}</TableCell>
|
<TableCell className="h-16 px-6 py-3 max-w-xs truncate text-sm">{webType.description || "-"}</TableCell>
|
||||||
<TableCell>
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
<Badge variant="outline" className="font-mono text-xs">
|
<Badge variant="outline" className="font-mono text-xs">
|
||||||
{webType.component_name || "TextWidget"}
|
{webType.component_name || "TextWidget"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
<Badge variant="secondary" className="font-mono text-xs">
|
<Badge variant="secondary" className="font-mono text-xs">
|
||||||
{webType.config_panel === "none" || !webType.config_panel ? "기본 설정" : webType.config_panel}
|
{webType.config_panel === "none" || !webType.config_panel ? "기본 설정" : webType.config_panel}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
<Badge variant={webType.is_active === "Y" ? "default" : "secondary"}>
|
<Badge variant={webType.is_active === "Y" ? "default" : "secondary"}>
|
||||||
{webType.is_active === "Y" ? "활성화" : "비활성화"}
|
{webType.is_active === "Y" ? "활성화" : "비활성화"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-muted-foreground text-sm">
|
<TableCell className="h-16 px-6 py-3 text-muted-foreground text-sm">
|
||||||
{webType.updated_date ? new Date(webType.updated_date).toLocaleDateString("ko-KR") : "-"}
|
{webType.updated_date ? new Date(webType.updated_date).toLocaleDateString("ko-KR") : "-"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link href={`/admin/standards/${webType.web_type}`}>
|
<Link href={`/admin/standards/${webType.web_type}`}>
|
||||||
<Button variant="ghost" size="sm">
|
<Button variant="ghost" size="sm">
|
||||||
|
|
@ -325,7 +324,7 @@ export default function WebTypesManagePage() {
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button variant="ghost" size="sm">
|
<Button variant="ghost" size="sm">
|
||||||
<Trash2 className="h-4 w-4 text-red-500" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
|
|
@ -341,7 +340,7 @@ export default function WebTypesManagePage() {
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={() => handleDelete(webType.web_type, webType.type_name)}
|
onClick={() => handleDelete(webType.web_type, webType.type_name)}
|
||||||
disabled={isDeleting}
|
disabled={isDeleting}
|
||||||
className="bg-red-600 hover:bg-red-700"
|
className="bg-destructive hover:bg-destructive/90"
|
||||||
>
|
>
|
||||||
{isDeleting ? "삭제 중..." : "삭제"}
|
{isDeleting ? "삭제 중..." : "삭제"}
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
|
|
@ -355,12 +354,11 @@ export default function WebTypesManagePage() {
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
|
||||||
|
|
||||||
{deleteError && (
|
{deleteError && (
|
||||||
<div className="mt-4 rounded-md border border-red-200 bg-red-50 p-4">
|
<div className="mt-4 rounded-md border border-destructive/30 bg-destructive/10 p-4">
|
||||||
<p className="text-red-600">
|
<p className="text-destructive">
|
||||||
삭제 중 오류가 발생했습니다: {deleteError instanceof Error ? deleteError.message : "알 수 없는 오류"}
|
삭제 중 오류가 발생했습니다: {deleteError instanceof Error ? deleteError.message : "알 수 없는 오류"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { useState, useEffect, useMemo, useCallback } from "react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Search, Database, RefreshCw, Settings, Plus, Activity, Trash2 } from "lucide-react";
|
import { Search, Database, RefreshCw, Settings, Plus, Activity, Trash2 } from "lucide-react";
|
||||||
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
|
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
|
||||||
|
|
@ -93,6 +94,9 @@ export default function TableManagementPage() {
|
||||||
const [tableToDelete, setTableToDelete] = useState<string>("");
|
const [tableToDelete, setTableToDelete] = useState<string>("");
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
|
||||||
|
// 선택된 테이블 목록 (체크박스)
|
||||||
|
const [selectedTableIds, setSelectedTableIds] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
// 최고 관리자 여부 확인 (회사코드가 "*"인 경우)
|
// 최고 관리자 여부 확인 (회사코드가 "*"인 경우)
|
||||||
const isSuperAdmin = user?.companyCode === "*";
|
const isSuperAdmin = user?.companyCode === "*";
|
||||||
|
|
||||||
|
|
@ -594,11 +598,91 @@ export default function TableManagementPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 체크박스 선택 핸들러
|
||||||
|
const handleTableCheck = (tableName: string, checked: boolean) => {
|
||||||
|
setSelectedTableIds((prev) => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
if (checked) {
|
||||||
|
newSet.add(tableName);
|
||||||
|
} else {
|
||||||
|
newSet.delete(tableName);
|
||||||
|
}
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 전체 선택/해제
|
||||||
|
const handleSelectAll = (checked: boolean) => {
|
||||||
|
if (checked) {
|
||||||
|
const filteredTables = tables.filter(
|
||||||
|
(table) =>
|
||||||
|
table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
(table.displayName && table.displayName.toLowerCase().includes(searchTerm.toLowerCase())),
|
||||||
|
);
|
||||||
|
setSelectedTableIds(new Set(filteredTables.map((table) => table.tableName)));
|
||||||
|
} else {
|
||||||
|
setSelectedTableIds(new Set());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 일괄 삭제 확인
|
||||||
|
const handleBulkDeleteClick = () => {
|
||||||
|
if (selectedTableIds.size === 0) return;
|
||||||
|
setDeleteDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 일괄 삭제 실행
|
||||||
|
const handleBulkDelete = async () => {
|
||||||
|
if (selectedTableIds.size === 0) return;
|
||||||
|
|
||||||
|
setIsDeleting(true);
|
||||||
|
try {
|
||||||
|
const tablesToDelete = Array.from(selectedTableIds);
|
||||||
|
let successCount = 0;
|
||||||
|
let failCount = 0;
|
||||||
|
|
||||||
|
for (const tableName of tablesToDelete) {
|
||||||
|
try {
|
||||||
|
const result = await ddlApi.dropTable(tableName);
|
||||||
|
if (result.success) {
|
||||||
|
successCount++;
|
||||||
|
// 삭제된 테이블이 선택된 테이블이었다면 선택 해제
|
||||||
|
if (selectedTable === tableName) {
|
||||||
|
setSelectedTable(null);
|
||||||
|
setColumns([]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
failCount++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
failCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (successCount > 0) {
|
||||||
|
toast.success(`${successCount}개의 테이블이 성공적으로 삭제되었습니다.`);
|
||||||
|
}
|
||||||
|
if (failCount > 0) {
|
||||||
|
toast.error(`${failCount}개의 테이블 삭제에 실패했습니다.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선택 초기화 및 테이블 목록 새로고침
|
||||||
|
setSelectedTableIds(new Set());
|
||||||
|
await loadTables();
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error("테이블 삭제 중 오류가 발생했습니다.");
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false);
|
||||||
|
setDeleteDialogOpen(false);
|
||||||
|
setTableToDelete("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-background flex min-h-screen flex-col">
|
<div className="bg-background flex h-screen flex-col">
|
||||||
<div className="space-y-6 p-6">
|
<div className="flex h-full flex-col space-y-6 overflow-hidden p-6">
|
||||||
{/* 페이지 헤더 */}
|
{/* 페이지 헤더 */}
|
||||||
<div className="space-y-2 border-b pb-4">
|
<div className="flex-shrink-0 space-y-2 border-b pb-4">
|
||||||
<div className="flex flex-col gap-2 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-2 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight">
|
<h1 className="text-3xl font-bold tracking-tight">
|
||||||
|
|
@ -664,28 +748,65 @@ export default function TableManagementPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex h-full gap-6">
|
<div className="flex h-full flex-1 gap-6 overflow-hidden">
|
||||||
{/* 좌측 사이드바: 테이블 목록 (20%) */}
|
{/* 좌측 사이드바: 테이블 목록 (20%) */}
|
||||||
<div className="w-[20%] border-r pr-6">
|
<div className="flex h-full w-[20%] flex-col border-r pr-4">
|
||||||
<div className="space-y-4">
|
<div className="flex h-full flex-col space-y-4">
|
||||||
<h2 className="flex items-center gap-2 text-lg font-semibold">
|
|
||||||
<Database className="text-muted-foreground h-5 w-5" />
|
|
||||||
{getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_NAME, "테이블 목록")}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{/* 검색 */}
|
{/* 검색 */}
|
||||||
<div className="relative">
|
<div className="flex-shrink-0">
|
||||||
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
|
<div className="relative">
|
||||||
<Input
|
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
|
||||||
placeholder={getTextFromUI(TABLE_MANAGEMENT_KEYS.SEARCH_PLACEHOLDER, "테이블 검색...")}
|
<Input
|
||||||
value={searchTerm}
|
placeholder={getTextFromUI(TABLE_MANAGEMENT_KEYS.SEARCH_PLACEHOLDER, "테이블 검색...")}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
value={searchTerm}
|
||||||
className="h-10 pl-10 text-sm"
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
/>
|
className="h-10 pl-10 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 테이블 목록 */}
|
{/* 테이블 목록 */}
|
||||||
<div className="space-y-3">
|
<div className="flex-1 space-y-3 overflow-y-auto">
|
||||||
|
{/* 전체 선택 및 일괄 삭제 (최고 관리자만) */}
|
||||||
|
{isSuperAdmin && (
|
||||||
|
<div className="flex items-center justify-between border-b pb-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
tables.filter(
|
||||||
|
(table) =>
|
||||||
|
table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
(table.displayName && table.displayName.toLowerCase().includes(searchTerm.toLowerCase())),
|
||||||
|
).length > 0 &&
|
||||||
|
tables
|
||||||
|
.filter(
|
||||||
|
(table) =>
|
||||||
|
table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
(table.displayName && table.displayName.toLowerCase().includes(searchTerm.toLowerCase())),
|
||||||
|
)
|
||||||
|
.every((table) => selectedTableIds.has(table.tableName))
|
||||||
|
}
|
||||||
|
onCheckedChange={handleSelectAll}
|
||||||
|
aria-label="전체 선택"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{selectedTableIds.size > 0 && `${selectedTableIds.size}개 선택됨`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{selectedTableIds.size > 0 && (
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleBulkDeleteClick}
|
||||||
|
className="h-8 gap-2 text-xs"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
삭제
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
|
|
@ -707,40 +828,44 @@ export default function TableManagementPage() {
|
||||||
.map((table) => (
|
.map((table) => (
|
||||||
<div
|
<div
|
||||||
key={table.tableName}
|
key={table.tableName}
|
||||||
className={`bg-card rounded-lg border p-4 shadow-sm transition-all ${
|
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
|
||||||
|
? { border: "2px solid #000000" }
|
||||||
|
: { border: "2px solid transparent" }
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className="cursor-pointer" onClick={() => handleTableSelect(table.tableName)}>
|
<div className="flex items-start gap-3">
|
||||||
<h4 className="text-sm font-semibold">{table.displayName || table.tableName}</h4>
|
{/* 체크박스 (최고 관리자만) */}
|
||||||
<p className="text-muted-foreground mt-1 text-xs">
|
{isSuperAdmin && (
|
||||||
{table.description || getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_DESCRIPTION, "설명 없음")}
|
<Checkbox
|
||||||
</p>
|
checked={selectedTableIds.has(table.tableName)}
|
||||||
<div className="mt-2 flex items-center justify-between border-t pt-2">
|
onCheckedChange={(checked) => handleTableCheck(table.tableName, checked as boolean)}
|
||||||
<span className="text-muted-foreground text-xs">컬럼</span>
|
aria-label={`${table.displayName || table.tableName} 선택`}
|
||||||
<Badge variant="secondary" className="text-xs">
|
className="mt-0.5"
|
||||||
{table.columnCount}
|
onClick={(e) => e.stopPropagation()}
|
||||||
</Badge>
|
/>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className="flex-1 cursor-pointer"
|
||||||
|
onClick={() => handleTableSelect(table.tableName)}
|
||||||
|
>
|
||||||
|
<h4 className="text-sm font-semibold">{table.displayName || table.tableName}</h4>
|
||||||
|
<p className="text-muted-foreground mt-1 text-xs">
|
||||||
|
{table.description || getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_DESCRIPTION, "설명 없음")}
|
||||||
|
</p>
|
||||||
|
<div className="mt-2 flex items-center justify-between border-t pt-2">
|
||||||
|
<span className="text-muted-foreground text-xs">컬럼</span>
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{table.columnCount}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 삭제 버튼 (최고 관리자만) */}
|
|
||||||
{isSuperAdmin && (
|
|
||||||
<div className="mt-2 border-t pt-2">
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
size="sm"
|
|
||||||
className="h-8 w-full gap-2 text-xs"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleDeleteTableClick(table.tableName);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-3 w-3" />
|
|
||||||
테이블 삭제
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|
@ -749,16 +874,11 @@ export default function TableManagementPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 우측 메인 영역: 컬럼 타입 관리 (80%) */}
|
{/* 우측 메인 영역: 컬럼 타입 관리 (80%) */}
|
||||||
<div className="w-[80%] pl-0">
|
<div className="flex h-full w-[80%] flex-col overflow-hidden pl-0">
|
||||||
<div className="flex h-full flex-col space-y-4">
|
<div className="flex h-full flex-col space-y-4 overflow-hidden">
|
||||||
<h2 className="flex items-center gap-2 text-xl font-semibold">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<Settings className="text-muted-foreground h-5 w-5" />
|
|
||||||
{selectedTable ? <>테이블 설정 - {selectedTable}</> : "테이블 타입 관리"}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-hidden">
|
|
||||||
{!selectedTable ? (
|
{!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">
|
<div className="flex flex-col items-center gap-2 text-center">
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
{getTextFromUI(TABLE_MANAGEMENT_KEYS.SELECT_TABLE_PLACEHOLDER, "테이블을 선택해주세요")}
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.SELECT_TABLE_PLACEHOLDER, "테이블을 선택해주세요")}
|
||||||
|
|
@ -801,19 +921,19 @@ export default function TableManagementPage() {
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* 컬럼 헤더 */}
|
{/* 컬럼 헤더 */}
|
||||||
<div className="text-foreground flex items-center border-b pb-2 text-sm font-semibold">
|
<div className="text-foreground flex h-12 items-center border-b px-6 py-3 text-sm font-semibold">
|
||||||
<div className="w-40 px-4">컬럼명</div>
|
<div className="w-40 pr-4">컬럼명</div>
|
||||||
<div className="w-48 px-4">라벨</div>
|
<div className="w-48 px-4">라벨</div>
|
||||||
<div className="w-48 px-4">입력 타입</div>
|
<div className="w-48 pr-6">입력 타입</div>
|
||||||
<div className="flex-1 px-4" style={{ maxWidth: "calc(100% - 808px)" }}>
|
<div className="flex-1 pl-6" style={{ maxWidth: "calc(100% - 808px)" }}>
|
||||||
상세 설정
|
상세 설정
|
||||||
</div>
|
</div>
|
||||||
<div className="w-80 px-4">설명</div>
|
<div className="w-80 pl-4">설명</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 컬럼 리스트 */}
|
{/* 컬럼 리스트 */}
|
||||||
<div
|
<div
|
||||||
className="max-h-96 overflow-y-auto rounded-lg border"
|
className="max-h-96 overflow-y-auto"
|
||||||
onScroll={(e) => {
|
onScroll={(e) => {
|
||||||
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
|
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
|
||||||
// 스크롤이 끝에 가까워지면 더 많은 데이터 로드
|
// 스크롤이 끝에 가까워지면 더 많은 데이터 로드
|
||||||
|
|
@ -825,9 +945,9 @@ export default function TableManagementPage() {
|
||||||
{columns.map((column, index) => (
|
{columns.map((column, index) => (
|
||||||
<div
|
<div
|
||||||
key={column.columnName}
|
key={column.columnName}
|
||||||
className="hover:bg-muted/50 flex items-center border-b py-2 transition-colors"
|
className="bg-background hover:bg-muted/50 flex min-h-16 items-center border-b px-6 py-3 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="w-40 px-4">
|
<div className="w-40 pr-4">
|
||||||
<div className="font-mono text-sm">{column.columnName}</div>
|
<div className="font-mono text-sm">{column.columnName}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-48 px-4">
|
<div className="w-48 px-4">
|
||||||
|
|
@ -838,7 +958,7 @@ export default function TableManagementPage() {
|
||||||
className="h-8 text-xs"
|
className="h-8 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-48 px-4">
|
<div className="w-48 pr-6">
|
||||||
<Select
|
<Select
|
||||||
value={column.inputType || "text"}
|
value={column.inputType || "text"}
|
||||||
onValueChange={(value) => handleInputTypeChange(column.columnName, value)}
|
onValueChange={(value) => handleInputTypeChange(column.columnName, value)}
|
||||||
|
|
@ -855,7 +975,7 @@ export default function TableManagementPage() {
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 px-4" style={{ maxWidth: "calc(100% - 808px)" }}>
|
<div className="flex-1 pl-6" style={{ maxWidth: "calc(100% - 808px)" }}>
|
||||||
{/* 웹 타입이 'code'인 경우 공통코드 선택 */}
|
{/* 웹 타입이 'code'인 경우 공통코드 선택 */}
|
||||||
{column.inputType === "code" && (
|
{column.inputType === "code" && (
|
||||||
<Select
|
<Select
|
||||||
|
|
@ -878,58 +998,98 @@ export default function TableManagementPage() {
|
||||||
)}
|
)}
|
||||||
{/* 웹 타입이 'entity'인 경우 참조 테이블 선택 */}
|
{/* 웹 타입이 'entity'인 경우 참조 테이블 선택 */}
|
||||||
{column.inputType === "entity" && (
|
{column.inputType === "entity" && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-2">
|
||||||
{/* Entity 타입 설정 - 가로 배치 */}
|
<div className="grid grid-cols-3 gap-2">
|
||||||
<div className="border-primary/20 bg-primary/5 rounded-lg border p-2">
|
{/* 참조 테이블 */}
|
||||||
<div className="mb-2 flex items-center gap-2">
|
<div>
|
||||||
<span className="text-primary text-xs font-medium">Entity 설정</span>
|
<label className="text-muted-foreground mb-1 block text-xs">
|
||||||
|
참조 테이블
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={column.referenceTable || "none"}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
handleDetailSettingsChange(column.columnName, "entity", value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="bg-background h-8 text-xs">
|
||||||
|
<SelectValue placeholder="선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{referenceTableOptions.map((option, index) => (
|
||||||
|
<SelectItem
|
||||||
|
key={`entity-${option.value}-${index}`}
|
||||||
|
value={option.value}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{option.label}</span>
|
||||||
|
<span className="text-muted-foreground text-xs">
|
||||||
|
{option.value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-2">
|
{/* 조인 컬럼 */}
|
||||||
{/* 참조 테이블 */}
|
{column.referenceTable && column.referenceTable !== "none" && (
|
||||||
<div>
|
<div>
|
||||||
<label className="text-muted-foreground mb-1 block text-xs">
|
<label className="text-muted-foreground mb-1 block text-xs">
|
||||||
참조 테이블
|
조인 컬럼
|
||||||
</label>
|
</label>
|
||||||
<Select
|
<Select
|
||||||
value={column.referenceTable || "none"}
|
value={column.referenceColumn || "none"}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
handleDetailSettingsChange(column.columnName, "entity", value)
|
handleDetailSettingsChange(
|
||||||
|
column.columnName,
|
||||||
|
"entity_reference_column",
|
||||||
|
value,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="bg-background h-8 text-xs">
|
<SelectTrigger className="bg-background h-8 text-xs">
|
||||||
<SelectValue placeholder="선택" />
|
<SelectValue placeholder="선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{referenceTableOptions.map((option, index) => (
|
<SelectItem value="none">-- 선택 안함 --</SelectItem>
|
||||||
|
{referenceTableColumns[column.referenceTable]?.map((refCol, index) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={`entity-${option.value}-${index}`}
|
key={`ref-col-${refCol.columnName}-${index}`}
|
||||||
value={option.value}
|
value={refCol.columnName}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col">
|
<span className="font-medium">{refCol.columnName}</span>
|
||||||
<span className="font-medium">{option.label}</span>
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
{option.value}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
{(!referenceTableColumns[column.referenceTable] ||
|
||||||
|
referenceTableColumns[column.referenceTable].length === 0) && (
|
||||||
|
<SelectItem value="loading" disabled>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="border-primary h-3 w-3 animate-spin rounded-full border border-t-transparent"></div>
|
||||||
|
로딩중
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 조인 컬럼 */}
|
{/* 표시 컬럼 */}
|
||||||
{column.referenceTable && column.referenceTable !== "none" && (
|
{column.referenceTable &&
|
||||||
|
column.referenceTable !== "none" &&
|
||||||
|
column.referenceColumn &&
|
||||||
|
column.referenceColumn !== "none" && (
|
||||||
<div>
|
<div>
|
||||||
<label className="text-muted-foreground mb-1 block text-xs">
|
<label className="text-muted-foreground mb-1 block text-xs">
|
||||||
조인 컬럼
|
표시 컬럼
|
||||||
</label>
|
</label>
|
||||||
<Select
|
<Select
|
||||||
value={column.referenceColumn || "none"}
|
value={column.displayColumn || "none"}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
handleDetailSettingsChange(
|
handleDetailSettingsChange(
|
||||||
column.columnName,
|
column.columnName,
|
||||||
"entity_reference_column",
|
"entity_display_column",
|
||||||
value,
|
value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -960,31 +1120,32 @@ export default function TableManagementPage() {
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 설정 완료 표시 - 간소화 */}
|
|
||||||
{column.referenceTable &&
|
|
||||||
column.referenceTable !== "none" &&
|
|
||||||
column.referenceColumn &&
|
|
||||||
column.referenceColumn !== "none" &&
|
|
||||||
column.displayColumn &&
|
|
||||||
column.displayColumn !== "none" && (
|
|
||||||
<div className="bg-primary/10 text-primary mt-1 flex items-center gap-1 rounded px-2 py-1 text-xs">
|
|
||||||
<span>✓</span>
|
|
||||||
<span className="truncate">
|
|
||||||
{column.columnName} → {column.referenceTable}.{column.displayColumn}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 설정 완료 표시 */}
|
||||||
|
{column.referenceTable &&
|
||||||
|
column.referenceTable !== "none" &&
|
||||||
|
column.referenceColumn &&
|
||||||
|
column.referenceColumn !== "none" &&
|
||||||
|
column.displayColumn &&
|
||||||
|
column.displayColumn !== "none" && (
|
||||||
|
<div className="bg-primary/10 text-primary mt-2 flex items-center gap-1 rounded px-2 py-1 text-xs">
|
||||||
|
<span>✓</span>
|
||||||
|
<span className="truncate">
|
||||||
|
{column.columnName} → {column.referenceTable}.{column.displayColumn}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* 다른 웹 타입인 경우 빈 공간 */}
|
{/* 다른 웹 타입인 경우 빈 공간 */}
|
||||||
{column.inputType !== "code" && column.inputType !== "entity" && (
|
{column.inputType !== "code" && column.inputType !== "entity" && (
|
||||||
<div className="text-muted-foreground flex h-8 items-center text-xs">-</div>
|
<div className="text-muted-foreground flex h-8 items-center justify-center text-xs">
|
||||||
|
-
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-80 px-4">
|
<div className="w-80 pl-4">
|
||||||
<Input
|
<Input
|
||||||
value={column.description || ""}
|
value={column.description || ""}
|
||||||
onChange={(e) => handleColumnChange(index, "description", e.target.value)}
|
onChange={(e) => handleColumnChange(index, "description", e.target.value)}
|
||||||
|
|
@ -1075,26 +1236,62 @@ export default function TableManagementPage() {
|
||||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-base sm:text-lg">테이블 삭제 확인</DialogTitle>
|
<DialogTitle className="text-base sm:text-lg">
|
||||||
|
{selectedTableIds.size > 0 ? "테이블 일괄 삭제 확인" : "테이블 삭제 확인"}
|
||||||
|
</DialogTitle>
|
||||||
<DialogDescription className="text-xs sm:text-sm">
|
<DialogDescription className="text-xs sm:text-sm">
|
||||||
정말로 테이블을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.
|
{selectedTableIds.size > 0 ? (
|
||||||
|
<>
|
||||||
|
선택된 <strong>{selectedTableIds.size}개</strong>의 테이블을 삭제하시겠습니까?
|
||||||
|
<br />
|
||||||
|
이 작업은 되돌릴 수 없습니다.
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
정말로 테이블을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-3 sm:space-y-4">
|
{selectedTableIds.size === 0 && tableToDelete && (
|
||||||
<div className="border-destructive/50 bg-destructive/10 rounded-lg border p-4">
|
<div className="space-y-3 sm:space-y-4">
|
||||||
<p className="text-destructive text-sm font-semibold">경고</p>
|
<div className="border-destructive/50 bg-destructive/10 rounded-lg border p-4">
|
||||||
<p className="text-destructive/80 mt-1.5 text-sm">
|
<p className="text-destructive text-sm font-semibold">경고</p>
|
||||||
테이블 <span className="font-mono font-bold">{tableToDelete}</span>과 모든 데이터가 영구적으로
|
<p className="text-destructive/80 mt-1.5 text-sm">
|
||||||
삭제됩니다.
|
테이블 <span className="font-mono font-bold">{tableToDelete}</span>과 모든 데이터가 영구적으로
|
||||||
</p>
|
삭제됩니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
|
{selectedTableIds.size > 0 && (
|
||||||
|
<div className="space-y-3 sm:space-y-4">
|
||||||
|
<div className="border-destructive/50 bg-destructive/10 rounded-lg border p-4">
|
||||||
|
<p className="text-destructive text-sm font-semibold">경고</p>
|
||||||
|
<p className="text-destructive/80 mt-1.5 text-sm">
|
||||||
|
다음 테이블들과 모든 데이터가 영구적으로 삭제됩니다:
|
||||||
|
</p>
|
||||||
|
<ul className="text-destructive/80 mt-2 list-disc pl-5 text-sm">
|
||||||
|
{Array.from(selectedTableIds).map((tableName) => (
|
||||||
|
<li key={tableName} className="font-mono">
|
||||||
|
{tableName}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<DialogFooter className="gap-2 sm:gap-0">
|
<DialogFooter className="gap-2 sm:gap-0">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setDeleteDialogOpen(false)}
|
onClick={() => {
|
||||||
|
setDeleteDialogOpen(false);
|
||||||
|
setTableToDelete("");
|
||||||
|
setSelectedTableIds(new Set());
|
||||||
|
}}
|
||||||
disabled={isDeleting}
|
disabled={isDeleting}
|
||||||
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||||
>
|
>
|
||||||
|
|
@ -1102,7 +1299,7 @@ export default function TableManagementPage() {
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={handleDeleteTable}
|
onClick={selectedTableIds.size > 0 ? handleBulkDelete : handleDeleteTable}
|
||||||
disabled={isDeleting}
|
disabled={isDeleting}
|
||||||
className="h-8 flex-1 gap-2 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
className="h-8 flex-1 gap-2 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -240,7 +240,7 @@ export default function TemplatesManagePage() {
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[60px]">
|
<TableHead className="h-12 px-6 py-3 w-[60px]">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -251,7 +251,7 @@ export default function TemplatesManagePage() {
|
||||||
<ArrowUpDown className="ml-1 h-3 w-3" />
|
<ArrowUpDown className="ml-1 h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -262,7 +262,7 @@ export default function TemplatesManagePage() {
|
||||||
<ArrowUpDown className="ml-1 h-3 w-3" />
|
<ArrowUpDown className="ml-1 h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -273,14 +273,14 @@ export default function TemplatesManagePage() {
|
||||||
<ArrowUpDown className="ml-1 h-3 w-3" />
|
<ArrowUpDown className="ml-1 h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead>카테고리</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">카테고리</TableHead>
|
||||||
<TableHead>설명</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">설명</TableHead>
|
||||||
<TableHead>아이콘</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">아이콘</TableHead>
|
||||||
<TableHead>기본 크기</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">기본 크기</TableHead>
|
||||||
<TableHead>공개 여부</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">공개 여부</TableHead>
|
||||||
<TableHead>활성화</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">활성화</TableHead>
|
||||||
<TableHead>수정일</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">수정일</TableHead>
|
||||||
<TableHead className="w-[200px]">작업</TableHead>
|
<TableHead className="h-12 px-6 py-3 w-[200px] text-sm font-semibold">작업</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|
@ -299,39 +299,39 @@ export default function TemplatesManagePage() {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
filteredAndSortedTemplates.map((template) => (
|
filteredAndSortedTemplates.map((template) => (
|
||||||
<TableRow key={template.template_code}>
|
<TableRow key={template.template_code} className="bg-background transition-colors hover:bg-muted/50">
|
||||||
<TableCell className="font-mono">{template.sort_order || 0}</TableCell>
|
<TableCell className="h-16 px-6 py-3 font-mono text-sm">{template.sort_order || 0}</TableCell>
|
||||||
<TableCell className="font-mono">{template.template_code}</TableCell>
|
<TableCell className="h-16 px-6 py-3 font-mono text-sm">{template.template_code}</TableCell>
|
||||||
<TableCell className="font-medium">
|
<TableCell className="h-16 px-6 py-3 font-medium text-sm">
|
||||||
{template.template_name}
|
{template.template_name}
|
||||||
{template.template_name_eng && (
|
{template.template_name_eng && (
|
||||||
<div className="text-muted-foreground text-xs">{template.template_name_eng}</div>
|
<div className="text-muted-foreground text-xs">{template.template_name_eng}</div>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
<Badge variant="secondary">{template.category}</Badge>
|
<Badge variant="secondary">{template.category}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="max-w-xs truncate">{template.description || "-"}</TableCell>
|
<TableCell className="h-16 px-6 py-3 max-w-xs truncate text-sm">{template.description || "-"}</TableCell>
|
||||||
<TableCell>
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
<div className="flex items-center justify-center">{renderIcon(template.icon_name)}</div>
|
<div className="flex items-center justify-center">{renderIcon(template.icon_name)}</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-mono text-xs">
|
<TableCell className="h-16 px-6 py-3 font-mono text-xs">
|
||||||
{template.default_size ? `${template.default_size.width}×${template.default_size.height}` : "-"}
|
{template.default_size ? `${template.default_size.width}×${template.default_size.height}` : "-"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
<Badge variant={template.is_public === "Y" ? "default" : "secondary"}>
|
<Badge variant={template.is_public === "Y" ? "default" : "secondary"}>
|
||||||
{template.is_public === "Y" ? "공개" : "비공개"}
|
{template.is_public === "Y" ? "공개" : "비공개"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
<Badge variant={template.is_active === "Y" ? "default" : "secondary"}>
|
<Badge variant={template.is_active === "Y" ? "default" : "secondary"}>
|
||||||
{template.is_active === "Y" ? "활성화" : "비활성화"}
|
{template.is_active === "Y" ? "활성화" : "비활성화"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-muted-foreground text-sm">
|
<TableCell className="h-16 px-6 py-3 text-muted-foreground text-sm">
|
||||||
{template.updated_date ? new Date(template.updated_date).toLocaleDateString("ko-KR") : "-"}
|
{template.updated_date ? new Date(template.updated_date).toLocaleDateString("ko-KR") : "-"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
<div className="flex space-x-1">
|
<div className="flex space-x-1">
|
||||||
<Button asChild size="sm" variant="ghost">
|
<Button asChild size="sm" variant="ghost">
|
||||||
<Link href={`/admin/templates/${template.template_code}`}>
|
<Link href={`/admin/templates/${template.template_code}`}>
|
||||||
|
|
|
||||||
|
|
@ -54,12 +54,12 @@ export function CompanyTable({ companies, isLoading, onEdit, onDelete }: Company
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{COMPANY_TABLE_COLUMNS.map((column) => (
|
{COMPANY_TABLE_COLUMNS.map((column) => (
|
||||||
<TableHead key={column.key} className="h-12 text-sm font-semibold">
|
<TableHead key={column.key} className="h-12 px-6 py-3 text-sm font-semibold">
|
||||||
{column.label}
|
{column.label}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
))}
|
))}
|
||||||
<TableHead className="h-12 text-sm font-semibold">디스크 사용량</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">디스크 사용량</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">작업</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">작업</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|
@ -129,27 +129,27 @@ export function CompanyTable({ companies, isLoading, onEdit, onDelete }: Company
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* 데스크톱 테이블 뷰 (lg 이상) */}
|
{/* 데스크톱 테이블 뷰 (lg 이상) */}
|
||||||
<div className="hidden bg-card shadow-sm lg:block">
|
<div className="hidden bg-card lg:block">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{COMPANY_TABLE_COLUMNS.map((column) => (
|
{COMPANY_TABLE_COLUMNS.map((column) => (
|
||||||
<TableHead key={column.key} className="h-12 text-sm font-semibold">
|
<TableHead key={column.key} className="h-12 px-6 py-3 text-sm font-semibold">
|
||||||
{column.label}
|
{column.label}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
))}
|
))}
|
||||||
<TableHead className="h-12 text-sm font-semibold">디스크 사용량</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">디스크 사용량</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">작업</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">작업</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{companies.map((company) => (
|
{companies.map((company) => (
|
||||||
<TableRow key={company.regdate + company.company_code} className="transition-colors hover:bg-muted/50">
|
<TableRow key={company.regdate + company.company_code} className="bg-background transition-colors hover:bg-muted/50">
|
||||||
<TableCell className="h-16 font-mono text-sm">{company.company_code}</TableCell>
|
<TableCell className="h-16 px-6 py-3 font-mono text-sm">{company.company_code}</TableCell>
|
||||||
<TableCell className="h-16 text-sm font-medium">{company.company_name}</TableCell>
|
<TableCell className="h-16 px-6 py-3 text-sm font-medium">{company.company_name}</TableCell>
|
||||||
<TableCell className="h-16 text-sm">{company.writer}</TableCell>
|
<TableCell className="h-16 px-6 py-3 text-sm">{company.writer}</TableCell>
|
||||||
<TableCell className="h-16">{formatDiskUsage(company)}</TableCell>
|
<TableCell className="h-16 px-6 py-3">{formatDiskUsage(company)}</TableCell>
|
||||||
<TableCell className="h-16">
|
<TableCell className="h-16 px-6 py-3">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,10 @@ export const MenuManagement: React.FC = () => {
|
||||||
const [selectedMenuId, setSelectedMenuId] = useState<string>("");
|
const [selectedMenuId, setSelectedMenuId] = useState<string>("");
|
||||||
const [selectedMenus, setSelectedMenus] = useState<Set<string>>(new Set());
|
const [selectedMenus, setSelectedMenus] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
// 메뉴 관리 화면용 로컬 상태 (모든 상태의 메뉴 표시)
|
||||||
|
const [localAdminMenus, setLocalAdminMenus] = useState<MenuItem[]>([]);
|
||||||
|
const [localUserMenus, setLocalUserMenus] = useState<MenuItem[]>([]);
|
||||||
|
|
||||||
// 다국어 텍스트 훅 사용
|
// 다국어 텍스트 훅 사용
|
||||||
// getMenuText는 더 이상 사용하지 않음 - getUITextSync만 사용
|
// getMenuText는 더 이상 사용하지 않음 - getUITextSync만 사용
|
||||||
const { userLang } = useMultiLang({ companyCode: "*" });
|
const { userLang } = useMultiLang({ companyCode: "*" });
|
||||||
|
|
@ -176,6 +180,7 @@ export const MenuManagement: React.FC = () => {
|
||||||
// 초기 로딩
|
// 초기 로딩
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadCompanies();
|
loadCompanies();
|
||||||
|
loadMenus(false); // 메뉴 목록 로드 (메뉴 관리 화면용 - 모든 상태 표시)
|
||||||
// 사용자 언어가 설정되지 않았을 때만 기본 텍스트 설정
|
// 사용자 언어가 설정되지 않았을 때만 기본 텍스트 설정
|
||||||
if (!userLang) {
|
if (!userLang) {
|
||||||
initializeDefaultTexts();
|
initializeDefaultTexts();
|
||||||
|
|
@ -373,12 +378,54 @@ export const MenuManagement: React.FC = () => {
|
||||||
};
|
};
|
||||||
}, [isCompanyDropdownOpen]);
|
}, [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) => {
|
const loadMenus = async (showLoading = true) => {
|
||||||
// console.log(`📋 메뉴 목록 조회 시작 (showLoading: ${showLoading})`);
|
// console.log(`📋 메뉴 목록 조회 시작 (showLoading: ${showLoading})`);
|
||||||
try {
|
try {
|
||||||
if (showLoading) {
|
if (showLoading) {
|
||||||
setLoading(true);
|
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();
|
await refreshMenus();
|
||||||
// console.log("📋 메뉴 목록 조회 성공");
|
// console.log("📋 메뉴 목록 조회 성공");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -558,7 +605,7 @@ export const MenuManagement: React.FC = () => {
|
||||||
|
|
||||||
const handleAddMenu = (parentId: string, menuType: string, level: number) => {
|
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);
|
const parentMenu = currentMenus.find((menu) => menu.objid === parentId);
|
||||||
|
|
||||||
setFormData({
|
setFormData({
|
||||||
|
|
@ -575,7 +622,7 @@ export const MenuManagement: React.FC = () => {
|
||||||
// console.log("🔧 메뉴 수정 시작 - menuId:", menuId);
|
// 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);
|
const menuToEdit = currentMenus.find((menu) => (menu.objid || menu.OBJID) === menuId);
|
||||||
|
|
||||||
if (menuToEdit) {
|
if (menuToEdit) {
|
||||||
|
|
@ -614,7 +661,7 @@ export const MenuManagement: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectAllMenus = (checked: boolean) => {
|
const handleSelectAllMenus = (checked: boolean) => {
|
||||||
const currentMenus = selectedMenuType === "admin" ? adminMenus : userMenus;
|
const currentMenus = selectedMenuType === "admin" ? localAdminMenus : localUserMenus;
|
||||||
if (checked) {
|
if (checked) {
|
||||||
// 모든 메뉴 선택 (최상위 메뉴 포함)
|
// 모든 메뉴 선택 (최상위 메뉴 포함)
|
||||||
setSelectedMenus(new Set(currentMenus.map((menu) => menu.objid || menu.OBJID || "")));
|
setSelectedMenus(new Set(currentMenus.map((menu) => menu.objid || menu.OBJID || "")));
|
||||||
|
|
@ -726,7 +773,8 @@ export const MenuManagement: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCurrentMenus = () => {
|
const getCurrentMenus = () => {
|
||||||
const currentMenus = selectedMenuType === "admin" ? adminMenus : userMenus;
|
// 메뉴 관리 화면용: 모든 상태의 메뉴 표시 (localAdminMenus/localUserMenus 사용)
|
||||||
|
const currentMenus = selectedMenuType === "admin" ? localAdminMenus : localUserMenus;
|
||||||
|
|
||||||
// 검색어 필터링
|
// 검색어 필터링
|
||||||
let filteredMenus = currentMenus;
|
let filteredMenus = currentMenus;
|
||||||
|
|
@ -755,6 +803,13 @@ export const MenuManagement: React.FC = () => {
|
||||||
setSelectedMenuType(type);
|
setSelectedMenuType(type);
|
||||||
setSelectedMenus(new Set()); // 선택된 메뉴 초기화
|
setSelectedMenus(new Set()); // 선택된 메뉴 초기화
|
||||||
setExpandedMenus(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) => {
|
const handleToggleExpand = (menuId: string) => {
|
||||||
|
|
@ -777,8 +832,8 @@ export const MenuManagement: React.FC = () => {
|
||||||
|
|
||||||
// uiTextsCount를 useMemo로 계산하여 상태 변경 시에만 재계산
|
// uiTextsCount를 useMemo로 계산하여 상태 변경 시에만 재계산
|
||||||
const uiTextsCount = useMemo(() => Object.keys(uiTexts).length, [uiTexts]);
|
const uiTextsCount = useMemo(() => Object.keys(uiTexts).length, [uiTexts]);
|
||||||
const adminMenusCount = useMemo(() => adminMenus?.length || 0, [adminMenus]);
|
const adminMenusCount = useMemo(() => localAdminMenus?.length || 0, [localAdminMenus]);
|
||||||
const userMenusCount = useMemo(() => userMenus?.length || 0, [userMenus]);
|
const userMenusCount = useMemo(() => localUserMenus?.length || 0, [localUserMenus]);
|
||||||
|
|
||||||
// 디버깅을 위한 간단한 상태 표시
|
// 디버깅을 위한 간단한 상태 표시
|
||||||
// console.log("🔍 MenuManagement 렌더링 상태:", {
|
// console.log("🔍 MenuManagement 렌더링 상태:", {
|
||||||
|
|
@ -823,7 +878,7 @@ export const MenuManagement: React.FC = () => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant={selectedMenuType === "admin" ? "default" : "secondary"}>
|
<Badge variant={selectedMenuType === "admin" ? "default" : "secondary"}>
|
||||||
{adminMenus.length}
|
{localAdminMenus.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -842,7 +897,7 @@ export const MenuManagement: React.FC = () => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant={selectedMenuType === "user" ? "default" : "secondary"}>
|
<Badge variant={selectedMenuType === "user" ? "default" : "secondary"}>
|
||||||
{userMenus.length}
|
{localUserMenus.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -145,10 +145,10 @@ export const MenuTable: React.FC<MenuTableProps> = ({
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{title && <h3 className="text-lg font-semibold">{title}</h3>}
|
{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">
|
<div className="max-h-[calc(100vh-350px)] overflow-auto">
|
||||||
<Table noWrapper>
|
<Table noWrapper>
|
||||||
<TableHeader className="sticky top-0 z-20 bg-background shadow-sm">
|
<TableHeader className="sticky top-0 z-20 bg-background">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="h-12 w-12 text-sm font-semibold">
|
<TableHead className="h-12 w-12 text-sm font-semibold">
|
||||||
<input
|
<input
|
||||||
|
|
|
||||||
|
|
@ -252,34 +252,34 @@ export function RestApiConnectionList() {
|
||||||
|
|
||||||
{/* 연결 목록 */}
|
{/* 연결 목록 */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="bg-card flex h-64 items-center justify-center shadow-sm">
|
<div className="flex h-64 items-center justify-center bg-card">
|
||||||
<div className="text-muted-foreground text-sm">로딩 중...</div>
|
<div className="text-sm text-muted-foreground">로딩 중...</div>
|
||||||
</div>
|
</div>
|
||||||
) : connections.length === 0 ? (
|
) : 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">
|
<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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-card shadow-sm">
|
<div className="bg-card">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow className="bg-background">
|
||||||
<TableHead className="h-12 text-sm font-semibold">연결명</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">연결명</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">기본 URL</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">기본 URL</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">인증 타입</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">인증 타입</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">헤더 수</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">헤더 수</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">상태</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">상태</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">마지막 테스트</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">마지막 테스트</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">연결 테스트</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">연결 테스트</TableHead>
|
||||||
<TableHead className="h-12 text-right text-sm font-semibold">작업</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-right text-sm font-semibold">작업</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{connections.map((connection) => (
|
{connections.map((connection) => (
|
||||||
<TableRow key={connection.id} className="hover:bg-muted/50 transition-colors">
|
<TableRow key={connection.id} className="bg-background transition-colors hover:bg-muted/50">
|
||||||
<TableCell className="h-16 text-sm">
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
<div className="max-w-[200px]">
|
<div className="max-w-[200px]">
|
||||||
<div className="truncate font-medium" title={connection.connection_name}>
|
<div className="truncate font-medium" title={connection.connection_name}>
|
||||||
{connection.connection_name}
|
{connection.connection_name}
|
||||||
|
|
@ -291,23 +291,23 @@ export function RestApiConnectionList() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</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}>
|
<div className="max-w-[300px] truncate" title={connection.base_url}>
|
||||||
{connection.base_url}
|
{connection.base_url}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</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>
|
<Badge variant="outline">{AUTH_TYPE_LABELS[connection.auth_type] || connection.auth_type}</Badge>
|
||||||
</TableCell>
|
</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}
|
{Object.keys(connection.default_headers || {}).length}
|
||||||
</TableCell>
|
</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"}>
|
<Badge variant={connection.is_active === "Y" ? "default" : "secondary"}>
|
||||||
{connection.is_active === "Y" ? "활성" : "비활성"}
|
{connection.is_active === "Y" ? "활성" : "비활성"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="h-16 text-sm">
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
{connection.last_test_date ? (
|
{connection.last_test_date ? (
|
||||||
<div>
|
<div>
|
||||||
<div>{new Date(connection.last_test_date).toLocaleDateString()}</div>
|
<div>{new Date(connection.last_test_date).toLocaleDateString()}</div>
|
||||||
|
|
@ -322,7 +322,7 @@ export function RestApiConnectionList() {
|
||||||
<span className="text-muted-foreground">-</span>
|
<span className="text-muted-foreground">-</span>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="h-16 text-sm">
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
@ -340,7 +340,7 @@ export function RestApiConnectionList() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="h-16 text-right">
|
<TableCell className="h-16 px-6 py-3 text-right">
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ export function UserAuthTable({ users, isLoading, paginationInfo, onEditAuth, on
|
||||||
// 로딩 스켈레톤
|
// 로딩 스켈레톤
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-card hidden shadow-sm lg:block">
|
<div className="bg-card hidden lg:block">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|
@ -123,7 +123,7 @@ export function UserAuthTable({ users, isLoading, paginationInfo, onEditAuth, on
|
||||||
// 빈 상태
|
// 빈 상태
|
||||||
if (users.length === 0) {
|
if (users.length === 0) {
|
||||||
return (
|
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>
|
<p className="text-muted-foreground text-sm">등록된 사용자가 없습니다.</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -133,7 +133,7 @@ export function UserAuthTable({ users, isLoading, paginationInfo, onEditAuth, on
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* 데스크톱 테이블 */}
|
{/* 데스크톱 테이블 */}
|
||||||
<div className="bg-card hidden shadow-sm lg:block">
|
<div className="bg-card hidden lg:block">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|
@ -182,7 +182,7 @@ export function UserAuthTable({ users, isLoading, paginationInfo, onEditAuth, on
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={user.userId}
|
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">
|
<div className="mb-4 flex items-start justify-between">
|
||||||
|
|
|
||||||
|
|
@ -113,11 +113,11 @@ export function UserTable({
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{USER_TABLE_COLUMNS.map((column) => (
|
{USER_TABLE_COLUMNS.map((column) => (
|
||||||
<TableHead key={column.key} style={{ width: column.width }} className="h-12 text-sm font-semibold">
|
<TableHead key={column.key} style={{ width: column.width }} className="h-12 px-6 py-3 text-sm font-semibold">
|
||||||
{column.label}
|
{column.label}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
))}
|
))}
|
||||||
<TableHead className="h-12 w-[200px] text-sm font-semibold">작업</TableHead>
|
<TableHead className="h-12 px-6 py-3 w-[200px] text-sm font-semibold">작업</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|
@ -186,34 +186,34 @@ export function UserTable({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* 데스크톱 테이블 뷰 (lg 이상) */}
|
{/* 데스크톱 테이블 뷰 (lg 이상) */}
|
||||||
<div className="bg-card hidden shadow-sm lg:block">
|
<div className="bg-card hidden lg:block">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{USER_TABLE_COLUMNS.map((column) => (
|
{USER_TABLE_COLUMNS.map((column) => (
|
||||||
<TableHead key={column.key} style={{ width: column.width }} className="h-12 text-sm font-semibold">
|
<TableHead key={column.key} style={{ width: column.width }} className="h-12 px-6 py-3 text-sm font-semibold">
|
||||||
{column.label}
|
{column.label}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
))}
|
))}
|
||||||
<TableHead className="h-12 w-[200px] text-sm font-semibold">작업</TableHead>
|
<TableHead className="h-12 px-6 py-3 w-[200px] text-sm font-semibold">작업</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{users.map((user, index) => (
|
{users.map((user, index) => (
|
||||||
<TableRow key={`${user.userId}-${index}`} className="hover:bg-muted/50 transition-colors">
|
<TableRow key={`${user.userId}-${index}`} className="bg-background transition-colors hover:bg-muted/50">
|
||||||
<TableCell className="h-16 font-mono text-sm font-medium">{getRowNumber(index)}</TableCell>
|
<TableCell className="h-16 px-6 py-3 font-mono text-sm font-medium">{getRowNumber(index)}</TableCell>
|
||||||
<TableCell className="h-16 font-mono text-sm">{user.sabun || "-"}</TableCell>
|
<TableCell className="h-16 px-6 py-3 font-mono text-sm">{user.sabun || "-"}</TableCell>
|
||||||
<TableCell className="h-16 text-sm font-medium">{user.companyCode || "-"}</TableCell>
|
<TableCell className="h-16 px-6 py-3 text-sm font-medium">{user.companyCode || "-"}</TableCell>
|
||||||
<TableCell className="h-16 text-sm font-medium">{user.deptName || "-"}</TableCell>
|
<TableCell className="h-16 px-6 py-3 text-sm font-medium">{user.deptName || "-"}</TableCell>
|
||||||
<TableCell className="h-16 text-sm font-medium">{user.positionName || "-"}</TableCell>
|
<TableCell className="h-16 px-6 py-3 text-sm font-medium">{user.positionName || "-"}</TableCell>
|
||||||
<TableCell className="h-16 font-mono text-sm">{user.userId}</TableCell>
|
<TableCell className="h-16 px-6 py-3 font-mono text-sm">{user.userId}</TableCell>
|
||||||
<TableCell className="h-16 text-sm font-medium">{user.userName}</TableCell>
|
<TableCell className="h-16 px-6 py-3 text-sm font-medium">{user.userName}</TableCell>
|
||||||
<TableCell className="h-16 text-sm">{user.tel || user.cellPhone || "-"}</TableCell>
|
<TableCell className="h-16 px-6 py-3 text-sm">{user.tel || user.cellPhone || "-"}</TableCell>
|
||||||
<TableCell className="h-16 max-w-[200px] truncate text-sm" title={user.email}>
|
<TableCell className="h-16 px-6 py-3 max-w-[200px] truncate text-sm" title={user.email}>
|
||||||
{user.email || "-"}
|
{user.email || "-"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="h-16 text-sm">{formatDate(user.regDate || "")}</TableCell>
|
<TableCell className="h-16 px-6 py-3 text-sm">{formatDate(user.regDate || "")}</TableCell>
|
||||||
<TableCell className="h-16">
|
<TableCell className="h-16 px-6 py-3">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Switch
|
<Switch
|
||||||
checked={user.status === "active"}
|
checked={user.status === "active"}
|
||||||
|
|
@ -222,7 +222,7 @@ export function UserTable({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="h-16">
|
<TableCell className="h-16 px-6 py-3">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
|
||||||
|
|
@ -146,12 +146,6 @@ export default function DataFlowList({ onLoadFlow }: DataFlowListProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<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">
|
<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 ? (
|
{loading ? (
|
||||||
<>
|
<>
|
||||||
{/* 데스크톱 테이블 스켈레톤 */}
|
{/* 데스크톱 테이블 스켈레톤 */}
|
||||||
<div className="hidden rounded-lg border bg-card shadow-sm lg:block">
|
<div className="hidden bg-card shadow-sm lg:block">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="border-b bg-muted/50 hover:bg-muted/50">
|
<TableRow className="bg-background">
|
||||||
<TableHead className="h-12 text-sm font-semibold">플로우명</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">플로우명</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">설명</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">설명</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">생성일</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">생성일</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">최근 수정</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">최근 수정</TableHead>
|
||||||
<TableHead className="h-12 text-right text-sm font-semibold">작업</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-right text-sm font-semibold">작업</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{Array.from({ length: 5 }).map((_, index) => (
|
{Array.from({ length: 5 }).map((_, index) => (
|
||||||
<TableRow key={index} className="border-b">
|
<TableRow key={index} className="bg-background">
|
||||||
<TableCell className="h-16">
|
<TableCell className="h-16 px-6 py-3">
|
||||||
<div className="h-4 w-32 animate-pulse rounded bg-muted"></div>
|
<div className="h-4 w-32 animate-pulse rounded bg-muted"></div>
|
||||||
</TableCell>
|
</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>
|
<div className="h-4 w-48 animate-pulse rounded bg-muted"></div>
|
||||||
</TableCell>
|
</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>
|
<div className="h-4 w-24 animate-pulse rounded bg-muted"></div>
|
||||||
</TableCell>
|
</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>
|
<div className="h-4 w-24 animate-pulse rounded bg-muted"></div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="h-16">
|
<TableCell className="h-16 px-6 py-3">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<div className="h-8 w-8 animate-pulse rounded bg-muted"></div>
|
<div className="h-8 w-8 animate-pulse rounded bg-muted"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -264,46 +258,46 @@ export default function DataFlowList({ onLoadFlow }: DataFlowListProps) {
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* 데스크톱 테이블 뷰 (lg 이상) */}
|
{/* 데스크톱 테이블 뷰 (lg 이상) */}
|
||||||
<div className="hidden rounded-lg border bg-card shadow-sm lg:block">
|
<div className="hidden bg-card shadow-sm lg:block">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="border-b bg-muted/50 hover:bg-muted/50">
|
<TableRow className="bg-background">
|
||||||
<TableHead className="h-12 text-sm font-semibold">플로우명</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">플로우명</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">설명</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">설명</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">생성일</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">생성일</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">최근 수정</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">최근 수정</TableHead>
|
||||||
<TableHead className="h-12 text-right text-sm font-semibold">작업</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-right text-sm font-semibold">작업</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{filteredFlows.map((flow) => (
|
{filteredFlows.map((flow) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={flow.flowId}
|
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)}
|
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">
|
<div className="flex items-center font-medium">
|
||||||
<Network className="mr-2 h-4 w-4 text-primary" />
|
<Network className="mr-2 h-4 w-4 text-primary" />
|
||||||
{flow.flowName}
|
{flow.flowName}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</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>
|
<div className="text-muted-foreground">{flow.flowDescription || "설명 없음"}</div>
|
||||||
</TableCell>
|
</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">
|
<div className="flex items-center text-muted-foreground">
|
||||||
<Calendar className="mr-1 h-3 w-3" />
|
<Calendar className="mr-1 h-3 w-3" />
|
||||||
{new Date(flow.createdAt).toLocaleDateString()}
|
{new Date(flow.createdAt).toLocaleDateString()}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</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">
|
<div className="flex items-center text-muted-foreground">
|
||||||
<Calendar className="mr-1 h-3 w-3" />
|
<Calendar className="mr-1 h-3 w-3" />
|
||||||
{new Date(flow.updatedAt).toLocaleDateString()}
|
{new Date(flow.updatedAt).toLocaleDateString()}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</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">
|
<div className="flex justify-end">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
|
|
|
||||||
|
|
@ -429,31 +429,28 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||||
{/* 활성 화면 탭 */}
|
{/* 활성 화면 탭 */}
|
||||||
<TabsContent value="active">
|
<TabsContent value="active">
|
||||||
{/* 데스크톱 테이블 뷰 (lg 이상) */}
|
{/* 데스크톱 테이블 뷰 (lg 이상) */}
|
||||||
<div className="bg-card hidden rounded-lg border shadow-sm lg:block">
|
<div className="bg-card hidden shadow-sm lg:block">
|
||||||
<div className="border-b p-6">
|
|
||||||
<h3 className="text-lg font-semibold">화면 목록 ({screens.length})</h3>
|
|
||||||
</div>
|
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="bg-muted/50 hover:bg-muted/50 border-b">
|
<TableRow>
|
||||||
<TableHead className="h-12 text-sm font-semibold">화면명</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">화면명</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">화면 코드</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">화면 코드</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">테이블명</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">테이블명</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">상태</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">상태</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">생성일</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">생성일</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">작업</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">작업</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{screens.map((screen) => (
|
{screens.map((screen) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={screen.screenId}
|
key={screen.screenId}
|
||||||
className={`hover:bg-muted/50 cursor-pointer border-b transition-colors ${
|
className={`bg-background hover:bg-muted/50 cursor-pointer border-b transition-colors ${
|
||||||
selectedScreen?.screenId === screen.screenId ? "border-primary/20 bg-accent" : ""
|
selectedScreen?.screenId === screen.screenId ? "border-primary/20 bg-accent" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => onDesignScreen(screen)}
|
onClick={() => onDesignScreen(screen)}
|
||||||
>
|
>
|
||||||
<TableCell className="h-16 cursor-pointer">
|
<TableCell className="h-16 px-6 py-3 cursor-pointer">
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">{screen.screenName}</div>
|
<div className="font-medium">{screen.screenName}</div>
|
||||||
{screen.description && (
|
{screen.description && (
|
||||||
|
|
@ -461,26 +458,26 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="h-16">
|
<TableCell className="h-16 px-6 py-3">
|
||||||
<Badge variant="outline" className="font-mono">
|
<Badge variant="outline" className="font-mono">
|
||||||
{screen.screenCode}
|
{screen.screenCode}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="h-16">
|
<TableCell className="h-16 px-6 py-3">
|
||||||
<span className="text-muted-foreground font-mono text-sm">
|
<span className="text-muted-foreground font-mono text-sm">
|
||||||
{screen.tableLabel || screen.tableName}
|
{screen.tableLabel || screen.tableName}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="h-16">
|
<TableCell className="h-16 px-6 py-3">
|
||||||
<Badge variant={screen.isActive === "Y" ? "default" : "secondary"}>
|
<Badge variant={screen.isActive === "Y" ? "default" : "secondary"}>
|
||||||
{screen.isActive === "Y" ? "활성" : "비활성"}
|
{screen.isActive === "Y" ? "활성" : "비활성"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="h-16">
|
<TableCell className="h-16 px-6 py-3">
|
||||||
<div className="text-muted-foreground text-sm">{screen.createdDate.toLocaleDateString()}</div>
|
<div className="text-muted-foreground text-sm">{screen.createdDate.toLocaleDateString()}</div>
|
||||||
<div className="text-muted-foreground text-xs">{screen.createdBy}</div>
|
<div className="text-muted-foreground text-xs">{screen.createdBy}</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="h-16">
|
<TableCell className="h-16 px-6 py-3">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||||
|
|
@ -671,52 +668,37 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||||
{/* 휴지통 탭 */}
|
{/* 휴지통 탭 */}
|
||||||
<TabsContent value="trash">
|
<TabsContent value="trash">
|
||||||
{/* 데스크톱 테이블 뷰 (lg 이상) */}
|
{/* 데스크톱 테이블 뷰 (lg 이상) */}
|
||||||
<div className="bg-card hidden rounded-lg border shadow-sm lg:block">
|
<div className="bg-card hidden shadow-sm lg:block">
|
||||||
<div className="flex items-center justify-between border-b p-6">
|
|
||||||
<h3 className="text-lg font-semibold">휴지통 ({deletedScreens.length})</h3>
|
|
||||||
{selectedScreenIds.length > 0 && (
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleBulkDelete}
|
|
||||||
disabled={bulkDeleting}
|
|
||||||
className="h-9 gap-2 text-sm font-medium"
|
|
||||||
>
|
|
||||||
<Trash className="h-4 w-4" />
|
|
||||||
{bulkDeleting ? "삭제 중..." : `선택된 ${selectedScreenIds.length}개 영구삭제`}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="bg-muted/50 hover:bg-muted/50 border-b">
|
<TableRow>
|
||||||
<TableHead className="h-12 w-12">
|
<TableHead className="h-12 w-12 px-6 py-3">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={deletedScreens.length > 0 && selectedScreenIds.length === deletedScreens.length}
|
checked={deletedScreens.length > 0 && selectedScreenIds.length === deletedScreens.length}
|
||||||
onCheckedChange={handleSelectAll}
|
onCheckedChange={handleSelectAll}
|
||||||
aria-label="전체 선택"
|
aria-label="전체 선택"
|
||||||
/>
|
/>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">화면명</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">화면명</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">화면 코드</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">화면 코드</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">테이블명</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">테이블명</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">삭제일</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">삭제일</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">삭제자</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">삭제자</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">삭제 사유</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">삭제 사유</TableHead>
|
||||||
<TableHead className="h-12 text-sm font-semibold">작업</TableHead>
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">작업</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{deletedScreens.map((screen) => (
|
{deletedScreens.map((screen) => (
|
||||||
<TableRow key={screen.screenId} className="hover:bg-muted/50 border-b transition-colors">
|
<TableRow key={screen.screenId} className="bg-background hover:bg-muted/50 border-b transition-colors">
|
||||||
<TableCell className="h-16">
|
<TableCell className="h-16 px-6 py-3">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedScreenIds.includes(screen.screenId)}
|
checked={selectedScreenIds.includes(screen.screenId)}
|
||||||
onCheckedChange={(checked) => handleScreenCheck(screen.screenId, checked as boolean)}
|
onCheckedChange={(checked) => handleScreenCheck(screen.screenId, checked as boolean)}
|
||||||
aria-label={`${screen.screenName} 선택`}
|
aria-label={`${screen.screenName} 선택`}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="h-16">
|
<TableCell className="h-16 px-6 py-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">{screen.screenName}</div>
|
<div className="font-medium">{screen.screenName}</div>
|
||||||
{screen.description && (
|
{screen.description && (
|
||||||
|
|
@ -724,28 +706,28 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="h-16">
|
<TableCell className="h-16 px-6 py-3">
|
||||||
<Badge variant="outline" className="font-mono">
|
<Badge variant="outline" className="font-mono">
|
||||||
{screen.screenCode}
|
{screen.screenCode}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="h-16">
|
<TableCell className="h-16 px-6 py-3">
|
||||||
<span className="text-muted-foreground font-mono text-sm">
|
<span className="text-muted-foreground font-mono text-sm">
|
||||||
{screen.tableLabel || screen.tableName}
|
{screen.tableLabel || screen.tableName}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="h-16">
|
<TableCell className="h-16 px-6 py-3">
|
||||||
<div className="text-muted-foreground text-sm">{screen.deletedDate?.toLocaleDateString()}</div>
|
<div className="text-muted-foreground text-sm">{screen.deletedDate?.toLocaleDateString()}</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="h-16">
|
<TableCell className="h-16 px-6 py-3">
|
||||||
<div className="text-muted-foreground text-sm">{screen.deletedBy}</div>
|
<div className="text-muted-foreground text-sm">{screen.deletedBy}</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="h-16">
|
<TableCell className="h-16 px-6 py-3">
|
||||||
<div className="text-muted-foreground max-w-32 truncate text-sm" title={screen.deleteReason}>
|
<div className="text-muted-foreground max-w-32 truncate text-sm" title={screen.deleteReason}>
|
||||||
{screen.deleteReason || "-"}
|
{screen.deleteReason || "-"}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="h-16">
|
<TableCell className="h-16 px-6 py-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
@ -789,7 +771,6 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||||
onCheckedChange={handleSelectAll}
|
onCheckedChange={handleSelectAll}
|
||||||
aria-label="전체 선택"
|
aria-label="전체 선택"
|
||||||
/>
|
/>
|
||||||
<h3 className="text-base font-semibold">휴지통 ({deletedScreens.length})</h3>
|
|
||||||
</div>
|
</div>
|
||||||
{selectedScreenIds.length > 0 && (
|
{selectedScreenIds.length > 0 && (
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ import {
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
|
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
|
||||||
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
|
|
||||||
interface FlowWidgetProps {
|
interface FlowWidgetProps {
|
||||||
component: FlowComponent;
|
component: FlowComponent;
|
||||||
|
|
@ -55,6 +56,7 @@ export function FlowWidget({
|
||||||
onFlowRefresh,
|
onFlowRefresh,
|
||||||
}: FlowWidgetProps) {
|
}: FlowWidgetProps) {
|
||||||
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||||
|
const { user } = useAuth(); // 사용자 정보 가져오기
|
||||||
|
|
||||||
// 🆕 전역 상태 관리
|
// 🆕 전역 상태 관리
|
||||||
const setSelectedStep = useFlowStepStore((state) => state.setSelectedStep);
|
const setSelectedStep = useFlowStepStore((state) => state.setSelectedStep);
|
||||||
|
|
@ -117,30 +119,64 @@ export function FlowWidget({
|
||||||
// 🆕 플로우 컴포넌트 ID (버튼이 이 플로우를 참조할 때 사용)
|
// 🆕 플로우 컴포넌트 ID (버튼이 이 플로우를 참조할 때 사용)
|
||||||
const flowComponentId = component.id;
|
const flowComponentId = component.id;
|
||||||
|
|
||||||
// 🆕 localStorage 키 생성
|
// 🆕 localStorage 키 생성 (사용자별로 저장)
|
||||||
const filterSettingKey = useMemo(() => {
|
const filterSettingKey = useMemo(() => {
|
||||||
if (!flowId || selectedStepId === null) return null;
|
if (!flowId || selectedStepId === null || !user?.userId) return null;
|
||||||
return `flowWidget_searchFilters_${flowId}_${selectedStepId}`;
|
return `flowWidget_searchFilters_${user.userId}_${flowId}_${selectedStepId}`;
|
||||||
}, [flowId, selectedStepId]);
|
}, [flowId, selectedStepId, user?.userId]);
|
||||||
|
|
||||||
// 🆕 저장된 필터 설정 불러오기
|
// 🆕 저장된 필터 설정 불러오기
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!filterSettingKey || allAvailableColumns.length === 0) return;
|
if (!filterSettingKey || stepDataColumns.length === 0 || !user?.userId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 현재 사용자의 필터 설정만 불러오기
|
||||||
const saved = localStorage.getItem(filterSettingKey);
|
const saved = localStorage.getItem(filterSettingKey);
|
||||||
if (saved) {
|
if (saved) {
|
||||||
const savedFilters = JSON.parse(saved);
|
const savedFilters = JSON.parse(saved);
|
||||||
setSearchFilterColumns(new Set(savedFilters));
|
// 현재 단계에 표시되는 컬럼만 필터링
|
||||||
|
const validFilters = savedFilters.filter((col: string) => stepDataColumns.includes(col));
|
||||||
|
setSearchFilterColumns(new Set(validFilters));
|
||||||
} else {
|
} else {
|
||||||
// 초기값: 빈 필터 (사용자가 선택해야 함)
|
// 초기값: 빈 필터 (사용자가 선택해야 함)
|
||||||
setSearchFilterColumns(new Set());
|
setSearchFilterColumns(new Set());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 이전 사용자의 필터 설정 정리 (사용자 ID가 다른 키들 제거)
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const currentUserId = user.userId;
|
||||||
|
const keysToRemove: string[] = [];
|
||||||
|
|
||||||
|
// localStorage의 모든 키를 확인
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
const key = localStorage.key(i);
|
||||||
|
if (key && key.startsWith("flowWidget_searchFilters_")) {
|
||||||
|
// 키 형식: flowWidget_searchFilters_${userId}_${flowId}_${stepId}
|
||||||
|
// split("_")를 하면 ["flowWidget", "searchFilters", "사용자ID", "플로우ID", "스텝ID"]
|
||||||
|
// 따라서 userId는 parts[2]입니다
|
||||||
|
const parts = key.split("_");
|
||||||
|
if (parts.length >= 3) {
|
||||||
|
const userIdFromKey = parts[2]; // flowWidget_searchFilters_ 다음이 userId
|
||||||
|
// 현재 사용자 ID와 다른 사용자의 설정은 제거
|
||||||
|
if (userIdFromKey !== currentUserId) {
|
||||||
|
keysToRemove.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이전 사용자의 설정 제거
|
||||||
|
if (keysToRemove.length > 0) {
|
||||||
|
keysToRemove.forEach(key => {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("필터 설정 불러오기 실패:", error);
|
console.error("필터 설정 불러오기 실패:", error);
|
||||||
setSearchFilterColumns(new Set());
|
setSearchFilterColumns(new Set());
|
||||||
}
|
}
|
||||||
}, [filterSettingKey, allAvailableColumns]);
|
}, [filterSettingKey, stepDataColumns, user?.userId]);
|
||||||
|
|
||||||
// 🆕 필터 설정 저장
|
// 🆕 필터 설정 저장
|
||||||
const saveFilterSettings = useCallback(() => {
|
const saveFilterSettings = useCallback(() => {
|
||||||
|
|
@ -174,14 +210,14 @@ export function FlowWidget({
|
||||||
|
|
||||||
// 🆕 전체 선택/해제
|
// 🆕 전체 선택/해제
|
||||||
const toggleAllFilters = useCallback(() => {
|
const toggleAllFilters = useCallback(() => {
|
||||||
if (searchFilterColumns.size === allAvailableColumns.length) {
|
if (searchFilterColumns.size === stepDataColumns.length) {
|
||||||
// 전체 해제
|
// 전체 해제
|
||||||
setSearchFilterColumns(new Set());
|
setSearchFilterColumns(new Set());
|
||||||
} else {
|
} else {
|
||||||
// 전체 선택
|
// 전체 선택
|
||||||
setSearchFilterColumns(new Set(allAvailableColumns));
|
setSearchFilterColumns(new Set(stepDataColumns));
|
||||||
}
|
}
|
||||||
}, [searchFilterColumns, allAvailableColumns]);
|
}, [searchFilterColumns, stepDataColumns]);
|
||||||
|
|
||||||
// 🆕 검색 초기화
|
// 🆕 검색 초기화
|
||||||
const handleClearSearch = useCallback(() => {
|
const handleClearSearch = useCallback(() => {
|
||||||
|
|
@ -638,59 +674,76 @@ export function FlowWidget({
|
||||||
<React.Fragment key={step.id}>
|
<React.Fragment key={step.id}>
|
||||||
{/* 스텝 카드 */}
|
{/* 스텝 카드 */}
|
||||||
<div
|
<div
|
||||||
className={`group bg-card relative w-full cursor-pointer rounded-lg border-2 p-4 shadow-sm transition-all duration-200 sm:w-auto sm:min-w-[180px] sm:rounded-xl sm:p-5 lg:min-w-[220px] lg:p-6 ${
|
className="group relative w-full cursor-pointer pb-4 transition-all duration-300 sm:w-auto sm:min-w-[200px] lg:min-w-[240px]"
|
||||||
selectedStepId === step.id
|
|
||||||
? "border-primary bg-primary/5 shadow-md"
|
|
||||||
: "border-border hover:border-primary/50 hover:shadow-md"
|
|
||||||
}`}
|
|
||||||
onClick={() => handleStepClick(step.id, step.stepName)}
|
onClick={() => handleStepClick(step.id, step.stepName)}
|
||||||
>
|
>
|
||||||
{/* 단계 번호 배지 */}
|
{/* 콘텐츠 */}
|
||||||
<div className="bg-primary/10 text-primary mb-2 inline-flex items-center rounded-full px-2 py-1 text-xs font-medium sm:mb-3 sm:px-3">
|
<div className="relative flex flex-col items-center justify-center gap-2 pb-5 sm:gap-2.5 sm:pb-6">
|
||||||
Step {step.stepOrder}
|
{/* 스텝 이름 */}
|
||||||
</div>
|
<h4
|
||||||
|
className={`text-base font-semibold leading-tight transition-colors duration-300 sm:text-lg lg:text-xl ${
|
||||||
|
selectedStepId === step.id ? "text-primary" : "text-foreground group-hover:text-primary/80"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{step.stepName}
|
||||||
|
</h4>
|
||||||
|
|
||||||
{/* 스텝 이름 */}
|
{/* 데이터 건수 */}
|
||||||
<h4 className="text-foreground mb-2 pr-8 text-base leading-tight font-semibold sm:text-lg">
|
{showStepCount && (
|
||||||
{step.stepName}
|
<div
|
||||||
</h4>
|
className={`flex items-center gap-1.5 transition-all duration-300 ${
|
||||||
|
selectedStepId === step.id
|
||||||
{/* 데이터 건수 */}
|
? "text-primary"
|
||||||
{showStepCount && (
|
: "text-muted-foreground group-hover:text-primary"
|
||||||
<div className="text-muted-foreground mt-2 flex items-center gap-2 text-xs sm:mt-3 sm:text-sm">
|
}`}
|
||||||
<div className="bg-muted flex h-7 items-center rounded-md px-2 sm:h-8 sm:px-3">
|
>
|
||||||
<span className="text-foreground text-sm font-semibold sm:text-base">
|
<span className="text-sm font-medium sm:text-base">
|
||||||
{stepCounts[step.id] || 0}
|
{stepCounts[step.id] || 0}
|
||||||
</span>
|
</span>
|
||||||
<span className="ml-1">건</span>
|
<span className="text-xs font-normal sm:text-sm">건</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{/* 선택 인디케이터 */}
|
{/* 하단 선 */}
|
||||||
{selectedStepId === step.id && (
|
<div
|
||||||
<div className="absolute top-3 right-3 sm:top-4 sm:right-4">
|
className={`h-0.5 transition-all duration-300 ${
|
||||||
<ChevronUp className="text-primary h-4 w-4 sm:h-5 sm:w-5" />
|
selectedStepId === step.id
|
||||||
</div>
|
? "bg-primary"
|
||||||
)}
|
: "bg-border group-hover:bg-primary/50"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 화살표 (마지막 스텝 제외) */}
|
{/* 화살표 (마지막 스텝 제외) */}
|
||||||
{index < steps.length - 1 && (
|
{index < steps.length - 1 && (
|
||||||
<div className="text-muted-foreground/40 flex shrink-0 items-center justify-center py-2 sm:py-0">
|
<div className="flex shrink-0 items-center justify-center py-2 sm:py-0">
|
||||||
{displayMode === "horizontal" ? (
|
{displayMode === "horizontal" ? (
|
||||||
<svg
|
<div className="flex items-center gap-1">
|
||||||
className="h-5 w-5 rotate-90 sm:h-6 sm:w-6 sm:rotate-0"
|
<div className="h-0.5 w-6 bg-border sm:w-8" />
|
||||||
fill="none"
|
<svg
|
||||||
viewBox="0 0 24 24"
|
className="h-4 w-4 text-muted-foreground sm:h-5 sm:w-5"
|
||||||
stroke="currentColor"
|
fill="none"
|
||||||
>
|
viewBox="0 0 24 24"
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
stroke="currentColor"
|
||||||
</svg>
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
<div className="h-0.5 w-6 bg-border sm:w-8" />
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<svg className="h-5 w-5 sm:h-6 sm:w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<div className="flex flex-col items-center gap-1">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
<div className="h-6 w-0.5 bg-border sm:h-8" />
|
||||||
</svg>
|
<svg
|
||||||
|
className="h-4 w-4 text-muted-foreground sm:h-5 sm:w-5"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
<div className="h-6 w-0.5 bg-border sm:h-8" />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -720,7 +773,7 @@ export function FlowWidget({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 🆕 필터 설정 버튼 */}
|
{/* 🆕 필터 설정 버튼 */}
|
||||||
{allAvailableColumns.length > 0 && (
|
{stepDataColumns.length > 0 && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -746,7 +799,7 @@ export function FlowWidget({
|
||||||
|
|
||||||
{/* 🆕 검색 필터 입력 영역 */}
|
{/* 🆕 검색 필터 입력 영역 */}
|
||||||
{searchFilterColumns.size > 0 && (
|
{searchFilterColumns.size > 0 && (
|
||||||
<div className="mt-4 space-y-3 p-4">
|
<div className="mt-2 space-y-3 p-4">
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
{Object.keys(searchValues).length > 0 && (
|
{Object.keys(searchValues).length > 0 && (
|
||||||
<Button variant="ghost" size="sm" onClick={handleClearSearch} className="h-7 text-xs">
|
<Button variant="ghost" size="sm" onClick={handleClearSearch} className="h-7 text-xs">
|
||||||
|
|
@ -756,7 +809,7 @@ export function FlowWidget({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6">
|
||||||
{Array.from(searchFilterColumns).map((col) => (
|
{Array.from(searchFilterColumns).map((col) => (
|
||||||
<div key={col} className="space-y-1.5">
|
<div key={col} className="space-y-1.5">
|
||||||
<Label htmlFor={`search-${col}`} className="text-xs">
|
<Label htmlFor={`search-${col}`} className="text-xs">
|
||||||
|
|
@ -1043,20 +1096,20 @@ export function FlowWidget({
|
||||||
<div className="bg-muted/50 flex items-center gap-3 rounded border p-3">
|
<div className="bg-muted/50 flex items-center gap-3 rounded border p-3">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="select-all-filters"
|
id="select-all-filters"
|
||||||
checked={searchFilterColumns.size === allAvailableColumns.length && allAvailableColumns.length > 0}
|
checked={searchFilterColumns.size === stepDataColumns.length && stepDataColumns.length > 0}
|
||||||
onCheckedChange={toggleAllFilters}
|
onCheckedChange={toggleAllFilters}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="select-all-filters" className="flex-1 cursor-pointer text-xs font-semibold sm:text-sm">
|
<Label htmlFor="select-all-filters" className="flex-1 cursor-pointer text-xs font-semibold sm:text-sm">
|
||||||
전체 선택/해제
|
전체 선택/해제
|
||||||
</Label>
|
</Label>
|
||||||
<span className="text-muted-foreground text-xs">
|
<span className="text-muted-foreground text-xs">
|
||||||
{searchFilterColumns.size} / {allAvailableColumns.length}개
|
{searchFilterColumns.size} / {stepDataColumns.length}개
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 컬럼 목록 */}
|
{/* 컬럼 목록 */}
|
||||||
<div className="max-h-[50vh] space-y-2 overflow-y-auto rounded border p-2">
|
<div className="max-h-[50vh] space-y-2 overflow-y-auto rounded border p-2">
|
||||||
{allAvailableColumns.map((col) => (
|
{stepDataColumns.map((col) => (
|
||||||
<div key={col} className="hover:bg-muted/50 flex items-center gap-3 rounded p-2">
|
<div key={col} className="hover:bg-muted/50 flex items-center gap-3 rounded p-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id={`filter-${col}`}
|
id={`filter-${col}`}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ const buttonVariants = cva(
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
default: "bg-background border border-foreground text-foreground shadow-xs hover:bg-muted/50",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
outline:
|
outline:
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ function TabsList({
|
||||||
<TabsPrimitive.List
|
<TabsPrimitive.List
|
||||||
data-slot="tabs-list"
|
data-slot="tabs-list"
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -42,7 +42,7 @@ function TabsTrigger({
|
||||||
<TabsPrimitive.Trigger
|
<TabsPrimitive.Trigger
|
||||||
data-slot="tabs-trigger"
|
data-slot="tabs-trigger"
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { Plus, X, GripVertical, ChevronDown, ChevronUp } from "lucide-react";
|
import { Plus, X, GripVertical, ChevronDown, ChevronUp } from "lucide-react";
|
||||||
import { RepeaterFieldGroupConfig, RepeaterData, RepeaterItemData, RepeaterFieldDefinition } from "@/types/repeater";
|
import { RepeaterFieldGroupConfig, RepeaterData, RepeaterItemData, RepeaterFieldDefinition } from "@/types/repeater";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
@ -285,9 +285,9 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
|
||||||
if (fields.length === 0) {
|
if (fields.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className={cn("space-y-4", className)}>
|
<div className={cn("space-y-4", className)}>
|
||||||
<div className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-orange-300 bg-orange-50 p-8 text-center">
|
<div className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-destructive/30 bg-destructive/5 p-8 text-center">
|
||||||
<p className="text-sm font-medium text-orange-900">필드가 정의되지 않았습니다</p>
|
<p className="text-sm font-medium text-destructive">필드가 정의되지 않았습니다</p>
|
||||||
<p className="mt-2 text-xs text-orange-700">속성 패널에서 필드를 추가하세요.</p>
|
<p className="mt-2 text-xs text-muted-foreground">속성 패널에서 필드를 추가하세요.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -297,8 +297,8 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className={cn("space-y-4", className)}>
|
<div className={cn("space-y-4", className)}>
|
||||||
<div className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 p-8 text-center">
|
<div className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-border bg-muted/30 p-8 text-center">
|
||||||
<p className="mb-4 text-sm text-gray-500">{emptyMessage}</p>
|
<p className="mb-4 text-sm text-muted-foreground">{emptyMessage}</p>
|
||||||
{!readonly && !disabled && items.length < maxItems && (
|
{!readonly && !disabled && items.length < maxItems && (
|
||||||
<Button type="button" onClick={handleAddItem} size="sm">
|
<Button type="button" onClick={handleAddItem} size="sm">
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
|
@ -318,82 +318,79 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
|
||||||
// 그리드/테이블 형식 렌더링
|
// 그리드/테이블 형식 렌더링
|
||||||
const renderGridLayout = () => {
|
const renderGridLayout = () => {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border bg-white">
|
<div className="bg-card">
|
||||||
{/* 테이블 헤더 */}
|
<Table>
|
||||||
<div
|
<TableHeader>
|
||||||
className="grid gap-2 border-b bg-gray-50 p-3 font-semibold"
|
<TableRow className="bg-background">
|
||||||
style={{
|
|
||||||
gridTemplateColumns: `40px ${allowReorder ? "40px " : ""}${fields.map(() => "1fr").join(" ")} 80px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{showIndex && <div className="text-center text-sm">#</div>}
|
|
||||||
{allowReorder && <div className="text-center text-sm"></div>}
|
|
||||||
{fields.map((field) => (
|
|
||||||
<div key={field.name} className="text-sm text-gray-700">
|
|
||||||
{field.label}
|
|
||||||
{field.required && <span className="ml-1 text-orange-500">*</span>}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className="text-center text-sm">작업</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 테이블 바디 */}
|
|
||||||
<div className="divide-y">
|
|
||||||
{items.map((item, itemIndex) => (
|
|
||||||
<div
|
|
||||||
key={itemIndex}
|
|
||||||
className={cn(
|
|
||||||
"grid gap-2 p-3 transition-colors hover:bg-gray-50",
|
|
||||||
draggedIndex === itemIndex && "bg-blue-50 opacity-50",
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
gridTemplateColumns: `40px ${allowReorder ? "40px " : ""}${fields.map(() => "1fr").join(" ")} 80px`,
|
|
||||||
}}
|
|
||||||
draggable={allowReorder && !readonly && !disabled}
|
|
||||||
onDragStart={() => handleDragStart(itemIndex)}
|
|
||||||
onDragOver={(e) => handleDragOver(e, itemIndex)}
|
|
||||||
onDrop={(e) => handleDrop(e, itemIndex)}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
>
|
|
||||||
{/* 인덱스 번호 */}
|
|
||||||
{showIndex && (
|
{showIndex && (
|
||||||
<div className="flex items-center justify-center text-sm font-medium text-gray-600">
|
<TableHead className="h-12 w-12 px-6 py-3 text-center text-sm font-semibold">#</TableHead>
|
||||||
{itemIndex + 1}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
{allowReorder && (
|
||||||
{/* 드래그 핸들 */}
|
<TableHead className="h-12 w-12 px-6 py-3 text-center text-sm font-semibold"></TableHead>
|
||||||
{allowReorder && !readonly && !disabled && (
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<GripVertical className="h-4 w-4 cursor-move text-gray-400" />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 필드들 */}
|
|
||||||
{fields.map((field) => (
|
{fields.map((field) => (
|
||||||
<div key={field.name} className="flex items-center">
|
<TableHead key={field.name} className="h-12 px-6 py-3 text-sm font-semibold">
|
||||||
{renderField(field, itemIndex, item[field.name])}
|
{field.label}
|
||||||
</div>
|
{field.required && <span className="ml-1 text-destructive">*</span>}
|
||||||
|
</TableHead>
|
||||||
))}
|
))}
|
||||||
|
<TableHead className="h-12 w-20 px-6 py-3 text-center text-sm font-semibold">작업</TableHead>
|
||||||
{/* 삭제 버튼 */}
|
</TableRow>
|
||||||
<div className="flex items-center justify-center">
|
</TableHeader>
|
||||||
{!readonly && !disabled && items.length > minItems && (
|
<TableBody>
|
||||||
<Button
|
{items.map((item, itemIndex) => (
|
||||||
type="button"
|
<TableRow
|
||||||
variant="ghost"
|
key={itemIndex}
|
||||||
size="icon"
|
className={cn(
|
||||||
onClick={() => handleRemoveItem(itemIndex)}
|
"bg-background transition-colors hover:bg-muted/50",
|
||||||
className="h-8 w-8 text-red-500 hover:bg-red-50 hover:text-red-700"
|
draggedIndex === itemIndex && "opacity-50",
|
||||||
title="항목 제거"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
draggable={allowReorder && !readonly && !disabled}
|
||||||
</div>
|
onDragStart={() => handleDragStart(itemIndex)}
|
||||||
))}
|
onDragOver={(e) => handleDragOver(e, itemIndex)}
|
||||||
</div>
|
onDrop={(e) => handleDrop(e, itemIndex)}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
>
|
||||||
|
{/* 인덱스 번호 */}
|
||||||
|
{showIndex && (
|
||||||
|
<TableCell className="h-16 px-6 py-3 text-center text-sm font-medium">
|
||||||
|
{itemIndex + 1}
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 드래그 핸들 */}
|
||||||
|
{allowReorder && !readonly && !disabled && (
|
||||||
|
<TableCell className="h-16 px-6 py-3 text-center">
|
||||||
|
<GripVertical className="h-4 w-4 cursor-move text-muted-foreground" />
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 필드들 */}
|
||||||
|
{fields.map((field) => (
|
||||||
|
<TableCell key={field.name} className="h-16 px-6 py-3">
|
||||||
|
{renderField(field, itemIndex, item[field.name])}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* 삭제 버튼 */}
|
||||||
|
<TableCell className="h-16 px-6 py-3 text-center">
|
||||||
|
{!readonly && !disabled && items.length > minItems && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => handleRemoveItem(itemIndex)}
|
||||||
|
className="h-8 w-8 text-destructive hover:bg-destructive/10 hover:text-destructive"
|
||||||
|
title="항목 제거"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -423,12 +420,12 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* 드래그 핸들 */}
|
{/* 드래그 핸들 */}
|
||||||
{allowReorder && !readonly && !disabled && (
|
{allowReorder && !readonly && !disabled && (
|
||||||
<GripVertical className="h-4 w-4 flex-shrink-0 cursor-move text-gray-400" />
|
<GripVertical className="h-4 w-4 flex-shrink-0 cursor-move text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 인덱스 번호 */}
|
{/* 인덱스 번호 */}
|
||||||
{showIndex && (
|
{showIndex && (
|
||||||
<CardTitle className="text-sm font-semibold text-gray-700">항목 {itemIndex + 1}</CardTitle>
|
<CardTitle className="text-sm font-semibold text-foreground">항목 {itemIndex + 1}</CardTitle>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -453,7 +450,7 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => handleRemoveItem(itemIndex)}
|
onClick={() => handleRemoveItem(itemIndex)}
|
||||||
className="h-8 w-8 text-red-500 hover:bg-red-50 hover:text-red-700"
|
className="h-8 w-8 text-destructive hover:bg-destructive/10 hover:text-destructive"
|
||||||
title="항목 제거"
|
title="항목 제거"
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
|
|
@ -467,9 +464,9 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
|
||||||
<div className={getFieldsLayoutClass()}>
|
<div className={getFieldsLayoutClass()}>
|
||||||
{fields.map((field) => (
|
{fields.map((field) => (
|
||||||
<div key={field.name} className="space-y-1" style={{ width: field.width }}>
|
<div key={field.name} className="space-y-1" style={{ width: field.width }}>
|
||||||
<label className="text-sm font-medium text-gray-700">
|
<label className="text-sm font-medium text-foreground">
|
||||||
{field.label}
|
{field.label}
|
||||||
{field.required && <span className="ml-1 text-orange-500">*</span>}
|
{field.required && <span className="ml-1 text-destructive">*</span>}
|
||||||
</label>
|
</label>
|
||||||
{renderField(field, itemIndex, item[field.name])}
|
{renderField(field, itemIndex, item[field.name])}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -500,7 +497,7 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 제한 안내 */}
|
{/* 제한 안내 */}
|
||||||
<div className="flex justify-between text-xs text-gray-500">
|
<div className="flex justify-between text-xs text-muted-foreground">
|
||||||
<span>현재: {items.length}개 항목</span>
|
<span>현재: {items.length}개 항목</span>
|
||||||
<span>
|
<span>
|
||||||
(최소: {minItems}, 최대: {maxItems})
|
(최소: {minItems}, 최대: {maxItems})
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ export function MenuProvider({ children }: { children: ReactNode }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 관리자 메뉴와 사용자 메뉴를 병렬로 로드
|
// 관리자 메뉴와 사용자 메뉴를 병렬로 로드
|
||||||
|
// 좌측 사이드바용: active만 표시
|
||||||
const [adminResponse, userResponse] = await Promise.all([menuApi.getAdminMenus(), menuApi.getUserMenus()]);
|
const [adminResponse, userResponse] = await Promise.all([menuApi.getAdminMenus(), menuApi.getUserMenus()]);
|
||||||
|
|
||||||
if (adminResponse.success && adminResponse.data) {
|
if (adminResponse.success && adminResponse.data) {
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ export interface ApiResponse<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const menuApi = {
|
export const menuApi = {
|
||||||
// 관리자 메뉴 목록 조회
|
// 관리자 메뉴 목록 조회 (좌측 사이드바용 - active만 표시)
|
||||||
getAdminMenus: async (): Promise<ApiResponse<MenuItem[]>> => {
|
getAdminMenus: async (): Promise<ApiResponse<MenuItem[]>> => {
|
||||||
const response = await apiClient.get("/admin/menus", { params: { menuType: "0" } });
|
const response = await apiClient.get("/admin/menus", { params: { menuType: "0" } });
|
||||||
if (response.data.success && response.data.data && response.data.data.length > 0) {
|
if (response.data.success && response.data.data && response.data.data.length > 0) {
|
||||||
|
|
@ -84,12 +84,24 @@ export const menuApi = {
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
// 사용자 메뉴 목록 조회
|
// 사용자 메뉴 목록 조회 (좌측 사이드바용 - active만 표시)
|
||||||
getUserMenus: async (): Promise<ApiResponse<MenuItem[]>> => {
|
getUserMenus: async (): Promise<ApiResponse<MenuItem[]>> => {
|
||||||
const response = await apiClient.get("/admin/menus", { params: { menuType: "1" } });
|
const response = await apiClient.get("/admin/menus", { params: { menuType: "1" } });
|
||||||
return response.data;
|
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>> => {
|
getMenuInfo: async (menuId: string): Promise<ApiResponse<MenuItem>> => {
|
||||||
const response = await apiClient.get(`/admin/menus/${menuId}`);
|
const response = await apiClient.get(`/admin/menus/${menuId}`);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue