- Modified the addCategoryValue function to allow menuObjid to be optional, accommodating scenarios where it may not be provided, such as in global management screens. - Adjusted related service and controller logic to handle the absence of menuObjid gracefully, ensuring that the application remains robust and user-friendly. - Enhanced the frontend components to reflect these changes, improving the overall user experience when adding category values across multiple companies.
632 lines
17 KiB
TypeScript
632 lines
17 KiB
TypeScript
import { Response } from "express";
|
|
import { AuthenticatedRequest } from "../types/auth";
|
|
import tableCategoryValueService from "../services/tableCategoryValueService";
|
|
import { logger } from "../utils/logger";
|
|
|
|
/**
|
|
* 테이블의 카테고리 컬럼 목록 조회
|
|
*/
|
|
export const getCategoryColumns = async (req: AuthenticatedRequest, res: Response) => {
|
|
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,
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 모든 테이블의 카테고리 컬럼 목록 조회 (Select 옵션 설정용)
|
|
*/
|
|
export const getAllCategoryColumns = async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
|
|
const columns = await tableCategoryValueService.getAllCategoryColumns(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: 비활성 값 포함 여부
|
|
*/
|
|
export const getCategoryValues = async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
const userCompanyCode = req.user!.companyCode;
|
|
const { tableName, columnName } = req.params;
|
|
const includeInactive = req.query.includeInactive === "true";
|
|
const menuObjid = req.query.menuObjid ? Number(req.query.menuObjid) : undefined;
|
|
const filterCompanyCode = req.query.filterCompanyCode as string | undefined;
|
|
|
|
// 최고관리자가 특정 회사 기준 필터링을 요청한 경우 해당 회사 코드 사용
|
|
const effectiveCompanyCode = (userCompanyCode === "*" && filterCompanyCode)
|
|
? filterCompanyCode
|
|
: userCompanyCode;
|
|
|
|
logger.info("카테고리 값 조회 요청", {
|
|
tableName,
|
|
columnName,
|
|
menuObjid,
|
|
companyCode: effectiveCompanyCode,
|
|
filterCompanyCode,
|
|
});
|
|
|
|
const values = await tableCategoryValueService.getCategoryValues(
|
|
tableName,
|
|
columnName,
|
|
effectiveCompanyCode,
|
|
includeInactive,
|
|
menuObjid
|
|
);
|
|
|
|
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 (필수)
|
|
* - 나머지 카테고리 값 정보
|
|
*/
|
|
export const addCategoryValue = async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const userId = req.user!.userId;
|
|
const { menuObjid, ...value } = req.body;
|
|
|
|
// menuObjid는 선택사항 — 옵션설정 등 전역 관리 화면에서는 없을 수 있음
|
|
|
|
logger.info("카테고리 값 추가 요청", {
|
|
tableName: value.tableName,
|
|
columnName: value.columnName,
|
|
menuObjid,
|
|
companyCode,
|
|
});
|
|
|
|
const newValue = await tableCategoryValueService.addCategoryValue(
|
|
value,
|
|
companyCode,
|
|
userId,
|
|
menuObjid ? Number(menuObjid) : null
|
|
);
|
|
|
|
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,
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 카테고리 값 수정
|
|
*/
|
|
export const updateCategoryValue = async (req: AuthenticatedRequest, res: Response) => {
|
|
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,
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 카테고리 값 삭제
|
|
*/
|
|
export const deleteCategoryValue = async (req: AuthenticatedRequest, res: Response) => {
|
|
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)
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: error.message || "카테고리 값 삭제 중 오류가 발생했습니다",
|
|
error: error.message,
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 카테고리 값 일괄 삭제
|
|
*/
|
|
export const bulkDeleteCategoryValues = async (
|
|
req: AuthenticatedRequest,
|
|
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,
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 카테고리 값 순서 변경
|
|
*/
|
|
export const reorderCategoryValues = async (req: AuthenticatedRequest, res: Response) => {
|
|
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,
|
|
});
|
|
}
|
|
};
|
|
|
|
// ================================================
|
|
// 컬럼 매핑 관련 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,
|
|
message: error.message || "컬럼 매핑 삭제 중 오류가 발생했습니다",
|
|
error: error.message,
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 카테고리 코드로 라벨 조회
|
|
*
|
|
* POST /api/table-categories/labels-by-codes
|
|
*
|
|
* Body:
|
|
* - valueCodes: 카테고리 코드 배열 (예: ["CATEGORY_767659DCUF", "CATEGORY_8292565608"])
|
|
*
|
|
* Response:
|
|
* - { [code]: label } 형태의 매핑 객체
|
|
*/
|
|
export const getCategoryLabelsByCodes = async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const { valueCodes } = req.body;
|
|
|
|
if (!valueCodes || !Array.isArray(valueCodes) || valueCodes.length === 0) {
|
|
return res.json({
|
|
success: true,
|
|
data: {},
|
|
});
|
|
}
|
|
|
|
logger.info("카테고리 코드로 라벨 조회", {
|
|
valueCodes,
|
|
companyCode,
|
|
});
|
|
|
|
const labels = await tableCategoryValueService.getCategoryLabelsByCodes(
|
|
valueCodes,
|
|
companyCode
|
|
);
|
|
|
|
return res.json({
|
|
success: true,
|
|
data: labels,
|
|
});
|
|
} catch (error: any) {
|
|
logger.error(`카테고리 라벨 조회 실패: ${error.message}`);
|
|
return res.status(500).json({
|
|
success: false,
|
|
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,
|
|
});
|
|
}
|
|
};
|
|
|