630 lines
17 KiB
TypeScript
630 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 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,
|
|
});
|
|
|
|
const values = await tableCategoryValueService.getCategoryValues(
|
|
tableName,
|
|
columnName,
|
|
companyCode,
|
|
includeInactive,
|
|
menuObjid // ← 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;
|
|
|
|
if (!menuObjid) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "menuObjid는 필수입니다",
|
|
});
|
|
}
|
|
|
|
logger.info("카테고리 값 추가 요청", {
|
|
tableName: value.tableName,
|
|
columnName: value.columnName,
|
|
menuObjid,
|
|
companyCode,
|
|
});
|
|
|
|
const newValue = await tableCategoryValueService.addCategoryValue(
|
|
value,
|
|
companyCode,
|
|
userId,
|
|
Number(menuObjid) // ← menuObjid 전달
|
|
);
|
|
|
|
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,
|
|
});
|
|
}
|
|
};
|
|
|