ERP-node/backend-node/src/controllers/tableCategoryValueControlle...

560 lines
15 KiB
TypeScript
Raw Normal View History

2025-11-07 10:18:34 +09:00
import { Response } from "express";
import { AuthenticatedRequest } from "../types/auth";
2025-11-05 15:23:57 +09:00
import tableCategoryValueService from "../services/tableCategoryValueService";
import { logger } from "../utils/logger";
/**
*
*/
2025-11-07 10:18:34 +09:00
export const getCategoryColumns = async (req: AuthenticatedRequest, res: Response) => {
2025-11-05 15:23:57 +09:00
try {
const companyCode = req.user!.companyCode;
const { tableName } = req.params;
const columns = await tableCategoryValueService.getCategoryColumns(
tableName,
companyCode
);
return res.json({
success: true,
data: columns,
});
} catch (error: any) {
logger.error(`카테고리 컬럼 조회 실패: ${error.message}`);
return res.status(500).json({
success: false,
message: "카테고리 컬럼 조회 중 오류가 발생했습니다",
error: error.message,
});
}
};
/**
* ( )
*
* Query Parameters:
* - menuObjid: 메뉴 OBJID (, )
* - includeInactive: 비활성
2025-11-05 15:23:57 +09:00
*/
2025-11-07 10:18:34 +09:00
export const getCategoryValues = async (req: AuthenticatedRequest, res: Response) => {
2025-11-05 15:23:57 +09:00
try {
const companyCode = req.user!.companyCode;
const { tableName, columnName } = req.params;
const includeInactive = req.query.includeInactive === "true";
const menuObjid = req.query.menuObjid ? Number(req.query.menuObjid) : undefined;
logger.info("카테고리 값 조회 요청", {
tableName,
columnName,
menuObjid,
companyCode,
});
2025-11-05 15:23:57 +09:00
const values = await tableCategoryValueService.getCategoryValues(
tableName,
columnName,
companyCode,
includeInactive,
menuObjid // ← menuObjid 전달
2025-11-05 15:23:57 +09:00
);
return res.json({
success: true,
data: values,
});
} catch (error: any) {
logger.error(`카테고리 값 조회 실패: ${error.message}`);
return res.status(500).json({
success: false,
message: "카테고리 값 조회 중 오류가 발생했습니다",
error: error.message,
});
}
};
/**
* ( )
*
* Body:
* - menuObjid: 메뉴 OBJID ()
* -
2025-11-05 15:23:57 +09:00
*/
2025-11-07 10:18:34 +09:00
export const addCategoryValue = async (req: AuthenticatedRequest, res: Response) => {
2025-11-05 15:23:57 +09:00
try {
const companyCode = req.user!.companyCode;
const userId = req.user!.userId;
const { menuObjid, ...value } = req.body;
if (!menuObjid) {
return res.status(400).json({
success: false,
message: "menuObjid는 필수입니다",
});
}
logger.info("카테고리 값 추가 요청", {
tableName: value.tableName,
columnName: value.columnName,
menuObjid,
companyCode,
});
2025-11-05 15:23:57 +09:00
const newValue = await tableCategoryValueService.addCategoryValue(
value,
companyCode,
userId,
Number(menuObjid) // ← menuObjid 전달
2025-11-05 15:23:57 +09:00
);
return res.status(201).json({
success: true,
data: newValue,
});
} catch (error: any) {
logger.error(`카테고리 값 추가 실패: ${error.message}`);
return res.status(500).json({
success: false,
message: error.message || "카테고리 값 추가 중 오류가 발생했습니다",
error: error.message,
});
}
};
/**
*
*/
2025-11-07 10:18:34 +09:00
export const updateCategoryValue = async (req: AuthenticatedRequest, res: Response) => {
2025-11-05 15:23:57 +09:00
try {
const companyCode = req.user!.companyCode;
const userId = req.user!.userId;
const valueId = parseInt(req.params.valueId);
const updates = req.body;
if (isNaN(valueId)) {
return res.status(400).json({
success: false,
message: "유효하지 않은 값 ID입니다",
});
}
const updatedValue = await tableCategoryValueService.updateCategoryValue(
valueId,
updates,
companyCode,
userId
);
return res.json({
success: true,
data: updatedValue,
});
} catch (error: any) {
logger.error(`카테고리 값 수정 실패: ${error.message}`);
return res.status(500).json({
success: false,
message: "카테고리 값 수정 중 오류가 발생했습니다",
error: error.message,
});
}
};
/**
*
*/
2025-11-07 10:18:34 +09:00
export const deleteCategoryValue = async (req: AuthenticatedRequest, res: Response) => {
2025-11-05 15:23:57 +09:00
try {
const companyCode = req.user!.companyCode;
const userId = req.user!.userId;
const valueId = parseInt(req.params.valueId);
if (isNaN(valueId)) {
return res.status(400).json({
success: false,
message: "유효하지 않은 값 ID입니다",
});
}
await tableCategoryValueService.deleteCategoryValue(
valueId,
companyCode,
userId
);
return res.json({
success: true,
message: "카테고리 값이 삭제되었습니다",
});
} catch (error: any) {
logger.error(`카테고리 값 삭제 실패: ${error.message}`);
// 사용 중인 경우 상세 에러 메시지 반환 (400)
if (error.message.includes("삭제할 수 없습니다")) {
return res.status(400).json({
success: false,
message: error.message,
});
}
// 기타 에러 (500)
2025-11-05 15:23:57 +09:00
return res.status(500).json({
success: false,
message: error.message || "카테고리 값 삭제 중 오류가 발생했습니다",
error: error.message,
});
}
};
/**
*
*/
export const bulkDeleteCategoryValues = async (
2025-11-07 10:22:49 +09:00
req: AuthenticatedRequest,
2025-11-05 15:23:57 +09:00
res: Response
) => {
try {
const companyCode = req.user!.companyCode;
const userId = req.user!.userId;
const { valueIds } = req.body;
if (!Array.isArray(valueIds) || valueIds.length === 0) {
return res.status(400).json({
success: false,
message: "삭제할 값 ID 목록이 필요합니다",
});
}
await tableCategoryValueService.bulkDeleteCategoryValues(
valueIds,
companyCode,
userId
);
return res.json({
success: true,
message: `${valueIds.length}개의 카테고리 값이 삭제되었습니다`,
});
} catch (error: any) {
logger.error(`카테고리 값 일괄 삭제 실패: ${error.message}`);
return res.status(500).json({
success: false,
message: "카테고리 값 일괄 삭제 중 오류가 발생했습니다",
error: error.message,
});
}
};
/**
*
*/
2025-11-07 10:18:34 +09:00
export const reorderCategoryValues = async (req: AuthenticatedRequest, res: Response) => {
2025-11-05 15:23:57 +09:00
try {
const companyCode = req.user!.companyCode;
const { orderedValueIds } = req.body;
if (!Array.isArray(orderedValueIds) || orderedValueIds.length === 0) {
return res.status(400).json({
success: false,
message: "순서 정보가 필요합니다",
});
}
await tableCategoryValueService.reorderCategoryValues(
orderedValueIds,
companyCode
);
return res.json({
success: true,
message: "카테고리 값 순서가 변경되었습니다",
});
} catch (error: any) {
logger.error(`카테고리 값 순서 변경 실패: ${error.message}`);
return res.status(500).json({
success: false,
message: "카테고리 값 순서 변경 중 오류가 발생했습니다",
error: error.message,
});
}
};
feat: 화면 복사 기능 개선 및 버튼 모달 설정 수정 ## 주요 변경사항 ### 1. 화면 복사 기능 강화 - 최고 관리자가 다른 회사로 화면 복사 가능하도록 개선 - 메인 화면과 연결된 모달 화면 자동 감지 및 일괄 복사 - 복사 시 버튼의 targetScreenId 자동 업데이트 - 일괄 이름 변경 기능 추가 (복사본 텍스트 제거) - 중복 화면명 체크 기능 추가 #### 백엔드 (screenManagementService.ts) - generateMultipleScreenCodes: 여러 화면 코드 일괄 생성 (Advisory Lock 사용) - detectLinkedModalScreens: edit 액션도 모달로 감지하도록 개선 - checkDuplicateScreenName: 중복 화면명 체크 API 추가 - copyScreenWithModals: 메인+모달 일괄 복사 및 버튼 업데이트 - updateButtonTargetScreenIds: 복사된 모달로 버튼 targetScreenId 업데이트 - updated_date 컬럼 제거 (screen_layouts 테이블에 존재하지 않음) #### 프론트엔드 (CopyScreenModal.tsx) - 회사 선택 UI 추가 (최고 관리자 전용) - 연결된 모달 화면 자동 감지 및 표시 - 일괄 이름 변경 기능 (텍스트 제거/추가) - 실시간 미리보기 - 중복 화면명 체크 ### 2. 버튼 설정 모달 화면 선택 개선 - 편집 중인 화면의 company_code 기준으로 화면 목록 조회 - 최고 관리자가 다른 회사 화면 편집 시 해당 회사의 모달 화면만 표시 - targetScreenId 문자열/숫자 타입 불일치 수정 #### 백엔드 (screenManagementController.ts) - getScreens API에 companyCode 쿼리 파라미터 추가 - 최고 관리자는 다른 회사의 화면 목록 조회 가능 #### 프론트엔드 - ButtonConfigPanel: currentScreenCompanyCode props 추가 - DetailSettingsPanel: currentScreenCompanyCode 전달 - UnifiedPropertiesPanel: currentScreenCompanyCode 전달 - ScreenDesigner: selectedScreen.companyCode 전달 - targetScreenId 비교 시 parseInt 처리 (문자열→숫자) ### 3. 카테고리 메뉴별 컬럼 분리 기능 - 메뉴별로 카테고리 컬럼을 독립적으로 관리 - 카테고리 컬럼 추가/삭제 시 메뉴 스코프 적용 ## 수정된 파일 - backend-node/src/services/screenManagementService.ts - backend-node/src/controllers/screenManagementController.ts - backend-node/src/routes/screenManagementRoutes.ts - frontend/components/screen/CopyScreenModal.tsx - frontend/components/screen/config-panels/ButtonConfigPanel.tsx - frontend/components/screen/panels/DetailSettingsPanel.tsx - frontend/components/screen/panels/UnifiedPropertiesPanel.tsx - frontend/components/screen/ScreenDesigner.tsx - frontend/lib/api/screen.ts
2025-11-13 12:17:10 +09:00
// ================================================
// 컬럼 매핑 관련 API (논리명 ↔ 물리명)
// ================================================
/**
*
*
* GET /api/categories/column-mapping/:tableName/:menuObjid
*
* .
*
* @returns { logical_column: physical_column }
*/
export const getColumnMapping = async (req: AuthenticatedRequest, res: Response) => {
try {
const companyCode = req.user!.companyCode;
const { tableName, menuObjid } = req.params;
if (!tableName || !menuObjid) {
return res.status(400).json({
success: false,
message: "tableName과 menuObjid는 필수입니다",
});
}
logger.info("컬럼 매핑 조회", {
tableName,
menuObjid,
companyCode,
});
const mapping = await tableCategoryValueService.getColumnMapping(
tableName,
Number(menuObjid),
companyCode
);
return res.json({
success: true,
data: mapping,
});
} catch (error: any) {
logger.error(`컬럼 매핑 조회 실패: ${error.message}`);
return res.status(500).json({
success: false,
message: "컬럼 매핑 조회 중 오류가 발생했습니다",
error: error.message,
});
}
};
/**
* /
*
* POST /api/categories/column-mapping
*
* Body:
* - tableName: 테이블명
* - logicalColumnName: 논리적 (: status_stock)
* - physicalColumnName: 물리적 (: status)
* - menuObjid: 메뉴 OBJID
* - description: 설명 ()
*/
export const createColumnMapping = async (req: AuthenticatedRequest, res: Response) => {
try {
const companyCode = req.user!.companyCode;
const userId = req.user!.userId;
const {
tableName,
logicalColumnName,
physicalColumnName,
menuObjid,
description,
} = req.body;
// 입력 검증
if (!tableName || !logicalColumnName || !physicalColumnName || !menuObjid) {
return res.status(400).json({
success: false,
message: "tableName, logicalColumnName, physicalColumnName, menuObjid는 필수입니다",
});
}
logger.info("컬럼 매핑 생성", {
tableName,
logicalColumnName,
physicalColumnName,
menuObjid,
companyCode,
});
const mapping = await tableCategoryValueService.createColumnMapping(
tableName,
logicalColumnName,
physicalColumnName,
Number(menuObjid),
companyCode,
userId,
description
);
return res.status(201).json({
success: true,
data: mapping,
message: "컬럼 매핑이 생성되었습니다",
});
} catch (error: any) {
logger.error(`컬럼 매핑 생성 실패: ${error.message}`);
return res.status(500).json({
success: false,
message: error.message || "컬럼 매핑 생성 중 오류가 발생했습니다",
error: error.message,
});
}
};
/**
*
*
* GET /api/categories/logical-columns/:tableName/:menuObjid
*
* .
* ( )
*/
export const getLogicalColumns = async (req: AuthenticatedRequest, res: Response) => {
try {
const companyCode = req.user!.companyCode;
const { tableName, menuObjid } = req.params;
if (!tableName || !menuObjid) {
return res.status(400).json({
success: false,
message: "tableName과 menuObjid는 필수입니다",
});
}
logger.info("논리적 컬럼 목록 조회", {
tableName,
menuObjid,
companyCode,
});
const columns = await tableCategoryValueService.getLogicalColumns(
tableName,
Number(menuObjid),
companyCode
);
return res.json({
success: true,
data: columns,
});
} catch (error: any) {
logger.error(`논리적 컬럼 목록 조회 실패: ${error.message}`);
return res.status(500).json({
success: false,
message: "논리적 컬럼 목록 조회 중 오류가 발생했습니다",
error: error.message,
});
}
};
/**
*
*
* DELETE /api/categories/column-mapping/:mappingId
*/
export const deleteColumnMapping = async (req: AuthenticatedRequest, res: Response) => {
try {
const companyCode = req.user!.companyCode;
const { mappingId } = req.params;
if (!mappingId) {
return res.status(400).json({
success: false,
message: "mappingId는 필수입니다",
});
}
logger.info("컬럼 매핑 삭제", {
mappingId,
companyCode,
});
await tableCategoryValueService.deleteColumnMapping(
Number(mappingId),
companyCode
);
return res.json({
success: true,
message: "컬럼 매핑이 삭제되었습니다",
});
} catch (error: any) {
logger.error(`컬럼 매핑 삭제 실패: ${error.message}`);
return res.status(500).json({
success: false,
message: error.message || "컬럼 매핑 삭제 중 오류가 발생했습니다",
error: error.message,
});
}
};
/**
* +
*
* DELETE /api/categories/column-mapping/:tableName/:columnName
*
*
*/
export const deleteColumnMappingsByColumn = async (req: AuthenticatedRequest, res: Response) => {
try {
const companyCode = req.user!.companyCode;
const { tableName, columnName } = req.params;
if (!tableName || !columnName) {
return res.status(400).json({
success: false,
message: "tableName과 columnName은 필수입니다",
});
}
logger.info("테이블+컬럼 기준 매핑 삭제", {
tableName,
columnName,
companyCode,
});
const deletedCount = await tableCategoryValueService.deleteColumnMappingsByColumn(
tableName,
columnName,
companyCode
);
return res.json({
success: true,
message: `${deletedCount}개의 컬럼 매핑이 삭제되었습니다`,
deletedCount,
});
} catch (error: any) {
logger.error(`테이블+컬럼 기준 매핑 삭제 실패: ${error.message}`);
return res.status(500).json({
success: false,
feat: 화면 복사 기능 개선 및 버튼 모달 설정 수정 ## 주요 변경사항 ### 1. 화면 복사 기능 강화 - 최고 관리자가 다른 회사로 화면 복사 가능하도록 개선 - 메인 화면과 연결된 모달 화면 자동 감지 및 일괄 복사 - 복사 시 버튼의 targetScreenId 자동 업데이트 - 일괄 이름 변경 기능 추가 (복사본 텍스트 제거) - 중복 화면명 체크 기능 추가 #### 백엔드 (screenManagementService.ts) - generateMultipleScreenCodes: 여러 화면 코드 일괄 생성 (Advisory Lock 사용) - detectLinkedModalScreens: edit 액션도 모달로 감지하도록 개선 - checkDuplicateScreenName: 중복 화면명 체크 API 추가 - copyScreenWithModals: 메인+모달 일괄 복사 및 버튼 업데이트 - updateButtonTargetScreenIds: 복사된 모달로 버튼 targetScreenId 업데이트 - updated_date 컬럼 제거 (screen_layouts 테이블에 존재하지 않음) #### 프론트엔드 (CopyScreenModal.tsx) - 회사 선택 UI 추가 (최고 관리자 전용) - 연결된 모달 화면 자동 감지 및 표시 - 일괄 이름 변경 기능 (텍스트 제거/추가) - 실시간 미리보기 - 중복 화면명 체크 ### 2. 버튼 설정 모달 화면 선택 개선 - 편집 중인 화면의 company_code 기준으로 화면 목록 조회 - 최고 관리자가 다른 회사 화면 편집 시 해당 회사의 모달 화면만 표시 - targetScreenId 문자열/숫자 타입 불일치 수정 #### 백엔드 (screenManagementController.ts) - getScreens API에 companyCode 쿼리 파라미터 추가 - 최고 관리자는 다른 회사의 화면 목록 조회 가능 #### 프론트엔드 - ButtonConfigPanel: currentScreenCompanyCode props 추가 - DetailSettingsPanel: currentScreenCompanyCode 전달 - UnifiedPropertiesPanel: currentScreenCompanyCode 전달 - ScreenDesigner: selectedScreen.companyCode 전달 - targetScreenId 비교 시 parseInt 처리 (문자열→숫자) ### 3. 카테고리 메뉴별 컬럼 분리 기능 - 메뉴별로 카테고리 컬럼을 독립적으로 관리 - 카테고리 컬럼 추가/삭제 시 메뉴 스코프 적용 ## 수정된 파일 - backend-node/src/services/screenManagementService.ts - backend-node/src/controllers/screenManagementController.ts - backend-node/src/routes/screenManagementRoutes.ts - frontend/components/screen/CopyScreenModal.tsx - frontend/components/screen/config-panels/ButtonConfigPanel.tsx - frontend/components/screen/panels/DetailSettingsPanel.tsx - frontend/components/screen/panels/UnifiedPropertiesPanel.tsx - frontend/components/screen/ScreenDesigner.tsx - frontend/lib/api/screen.ts
2025-11-13 12:17:10 +09:00
message: error.message || "컬럼 매핑 삭제 중 오류가 발생했습니다",
error: error.message,
});
}
};
/**
* 2
*
* GET /api/categories/second-level-menus
*
*
* 2
*/
export const getSecondLevelMenus = async (req: AuthenticatedRequest, res: Response) => {
try {
const companyCode = req.user!.companyCode;
logger.info("2레벨 메뉴 목록 조회", { companyCode });
const menus = await tableCategoryValueService.getSecondLevelMenus(companyCode);
return res.json({
success: true,
data: menus,
});
} catch (error: any) {
logger.error(`2레벨 메뉴 목록 조회 실패: ${error.message}`);
return res.status(500).json({
success: false,
message: "2레벨 메뉴 목록 조회 중 오류가 발생했습니다",
error: error.message,
});
}
};