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, }); } }; /** * 카테고리 값 목록 조회 (메뉴 스코프 적용) * * 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}`); 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, }); } }; /** * 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, }); } };