ERP-node/backend-node/src/controllers/tableManagementController.ts

2417 lines
73 KiB
TypeScript
Raw Normal View History

2025-08-25 14:08:08 +09:00
import { Request, Response } from "express";
2025-09-01 15:22:47 +09:00
import { Client } from "pg";
2025-08-25 14:08:08 +09:00
import { logger } from "../utils/logger";
import { AuthenticatedRequest } from "../types/auth";
import { ApiResponse } from "../types/common";
import { TableManagementService } from "../services/tableManagementService";
import {
TableInfo,
ColumnTypeInfo,
ColumnSettings,
TableListResponse,
ColumnListResponse,
ColumnSettingsResponse,
} from "../types/tableManagement";
2025-11-04 14:33:39 +09:00
import { query } from "../database/db"; // 🆕 query 함수 import
2025-08-25 14:08:08 +09:00
/**
*
*/
export async function getTableList(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
logger.info("=== 테이블 목록 조회 시작 ===");
2025-09-01 11:00:38 +09:00
const tableManagementService = new TableManagementService();
const tableList = await tableManagementService.getTableList();
2025-08-25 14:08:08 +09:00
2025-09-01 11:00:38 +09:00
logger.info(`테이블 목록 조회 결과: ${tableList.length}`);
2025-08-25 14:08:08 +09:00
2025-09-01 11:00:38 +09:00
const response: ApiResponse<TableInfo[]> = {
success: true,
message: "테이블 목록을 성공적으로 조회했습니다.",
data: tableList,
};
2025-08-25 14:08:08 +09:00
2025-09-01 11:00:38 +09:00
res.status(200).json(response);
2025-08-25 14:08:08 +09:00
} catch (error) {
logger.error("테이블 목록 조회 중 오류 발생:", error);
const response: ApiResponse<null> = {
success: false,
message: "테이블 목록 조회 중 오류가 발생했습니다.",
error: {
code: "TABLE_LIST_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
};
res.status(500).json(response);
}
}
/**
*
*/
export async function getColumnList(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName } = req.params;
2025-09-08 14:20:01 +09:00
const { page = 1, size = 50 } = req.query;
2025-11-06 17:01:13 +09:00
// 🔥 회사 코드 추출 (JWT에서 또는 DB에서 조회)
let companyCode = req.user?.companyCode;
2025-11-06 17:01:13 +09:00
if (!companyCode && req.user?.userId) {
// JWT에 없으면 DB에서 조회
const { query } = require("../database/db");
const userResult = await query(
`SELECT company_code FROM user_info WHERE user_id = $1`,
[req.user.userId]
);
companyCode = userResult[0]?.company_code;
logger.info(
`DB에서 회사 코드 조회 (컬럼 목록): ${req.user.userId}${companyCode}`
);
2025-11-06 17:01:13 +09:00
}
2025-09-08 14:20:01 +09:00
logger.info(
2025-11-06 17:01:13 +09:00
`=== 컬럼 정보 조회 시작: ${tableName} (page: ${page}, size: ${size}), company: ${companyCode} ===`
2025-09-08 14:20:01 +09:00
);
2025-08-25 14:08:08 +09:00
if (!tableName) {
const response: ApiResponse<null> = {
success: false,
message: "테이블명이 필요합니다.",
error: {
code: "MISSING_TABLE_NAME",
details: "테이블명 파라미터가 누락되었습니다.",
},
};
res.status(400).json(response);
return;
}
2025-09-01 11:00:38 +09:00
const tableManagementService = new TableManagementService();
2026-01-15 15:17:52 +09:00
2025-12-19 15:44:38 +09:00
// 🔥 캐시 버스팅: _t 파라미터가 있으면 캐시 무시
const bustCache = !!req.query._t;
2026-01-15 15:17:52 +09:00
2025-09-08 14:20:01 +09:00
const result = await tableManagementService.getColumnList(
tableName,
parseInt(page as string),
2025-11-06 17:01:13 +09:00
parseInt(size as string),
2025-12-19 15:44:38 +09:00
companyCode, // 🔥 회사 코드 전달
bustCache // 🔥 캐시 버스팅 옵션
2025-09-08 14:20:01 +09:00
);
2025-08-25 14:08:08 +09:00
2025-09-08 14:20:01 +09:00
logger.info(
`컬럼 정보 조회 결과: ${tableName}, ${result.columns.length}/${result.total}개 (${result.page}/${result.totalPages} 페이지)`
);
2025-08-25 14:08:08 +09:00
2025-09-08 14:20:01 +09:00
const response: ApiResponse<typeof result> = {
2025-09-01 11:00:38 +09:00
success: true,
message: "컬럼 목록을 성공적으로 조회했습니다.",
2025-09-08 14:20:01 +09:00
data: result,
2025-09-01 11:00:38 +09:00
};
2025-08-25 14:08:08 +09:00
2025-09-01 11:00:38 +09:00
res.status(200).json(response);
2025-08-25 14:08:08 +09:00
} catch (error) {
logger.error("컬럼 정보 조회 중 오류 발생:", error);
const response: ApiResponse<null> = {
success: false,
message: "컬럼 목록 조회 중 오류가 발생했습니다.",
error: {
code: "COLUMN_LIST_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
};
res.status(500).json(response);
}
}
/**
*
*/
export async function updateColumnSettings(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName, columnName } = req.params;
const settings: ColumnSettings = req.body;
2025-11-06 17:01:13 +09:00
// 🔥 회사 코드 추출 (JWT에서 또는 DB에서 조회)
let companyCode = req.user?.companyCode;
2025-11-06 17:01:13 +09:00
if (!companyCode && req.user?.userId) {
// JWT에 없으면 DB에서 조회
const { query } = require("../database/db");
const userResult = await query(
`SELECT company_code FROM user_info WHERE user_id = $1`,
[req.user.userId]
);
companyCode = userResult[0]?.company_code;
logger.info(`DB에서 회사 코드 조회: ${req.user.userId}${companyCode}`);
}
2025-08-25 14:08:08 +09:00
logger.info(
`=== 컬럼 설정 업데이트 시작: ${tableName}.${columnName}, company: ${companyCode} ===`
);
2025-08-25 14:08:08 +09:00
if (!tableName || !columnName) {
const response: ApiResponse<null> = {
success: false,
message: "테이블명과 컬럼명이 필요합니다.",
error: {
code: "MISSING_PARAMETERS",
details: "테이블명 또는 컬럼명 파라미터가 누락되었습니다.",
},
};
res.status(400).json(response);
return;
}
if (!settings) {
const response: ApiResponse<null> = {
success: false,
message: "컬럼 설정 정보가 필요합니다.",
error: {
code: "MISSING_SETTINGS",
details: "요청 본문에 컬럼 설정 정보가 누락되었습니다.",
},
};
res.status(400).json(response);
return;
}
2025-11-06 17:01:13 +09:00
if (!companyCode) {
logger.error(`회사 코드 누락: ${tableName}.${columnName}`, {
user: req.user,
hasUser: !!req.user,
userId: req.user?.userId,
companyCodeFromJWT: req.user?.companyCode,
});
const response: ApiResponse<null> = {
success: false,
message: "회사 코드를 찾을 수 없습니다.",
error: {
code: "MISSING_COMPANY_CODE",
details:
"사용자 정보에서 회사 코드를 찾을 수 없습니다. 관리자에게 문의하세요.",
2025-11-06 17:01:13 +09:00
},
};
res.status(400).json(response);
return;
}
2025-09-01 11:00:38 +09:00
const tableManagementService = new TableManagementService();
await tableManagementService.updateColumnSettings(
tableName,
columnName,
2025-11-06 17:01:13 +09:00
settings,
companyCode // 🔥 회사 코드 전달
2025-09-01 11:00:38 +09:00
);
2025-08-25 14:08:08 +09:00
logger.info(
`컬럼 설정 업데이트 완료: ${tableName}.${columnName}, company: ${companyCode}`
);
2025-08-25 14:08:08 +09:00
2025-09-01 11:00:38 +09:00
const response: ApiResponse<null> = {
success: true,
message: "컬럼 설정을 성공적으로 저장했습니다.",
};
2025-08-25 14:08:08 +09:00
2025-09-01 11:00:38 +09:00
res.status(200).json(response);
2025-08-25 14:08:08 +09:00
} catch (error) {
logger.error("컬럼 설정 업데이트 중 오류 발생:", error);
const response: ApiResponse<null> = {
success: false,
message: "컬럼 설정 저장 중 오류가 발생했습니다.",
error: {
code: "COLUMN_SETTINGS_UPDATE_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
};
res.status(500).json(response);
}
}
/**
*
*/
export async function updateAllColumnSettings(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName } = req.params;
const columnSettings: ColumnSettings[] = req.body;
2025-11-06 17:01:13 +09:00
// 🔥 회사 코드 추출 (JWT에서 또는 DB에서 조회)
let companyCode = req.user?.companyCode;
2025-11-06 17:01:13 +09:00
if (!companyCode && req.user?.userId) {
// JWT에 없으면 DB에서 조회
const { query } = require("../database/db");
const userResult = await query(
`SELECT company_code FROM user_info WHERE user_id = $1`,
[req.user.userId]
);
companyCode = userResult[0]?.company_code;
logger.info(`DB에서 회사 코드 조회: ${req.user.userId}${companyCode}`);
}
2025-08-25 14:08:08 +09:00
2025-11-06 17:01:13 +09:00
// 🔍 디버깅: 사용자 정보 출력
logger.info(`[DEBUG] req.user:`, JSON.stringify(req.user, null, 2));
logger.info(`[DEBUG] req.user?.companyCode: ${req.user?.companyCode}`);
logger.info(`[DEBUG] req.user?.userId: ${req.user?.userId}`);
logger.info(`[DEBUG] companyCode 최종값: ${companyCode}`);
logger.info(
`=== 전체 컬럼 설정 일괄 업데이트 시작: ${tableName}, company: ${companyCode} ===`
);
2025-08-25 14:08:08 +09:00
if (!tableName) {
const response: ApiResponse<null> = {
success: false,
message: "테이블명이 필요합니다.",
error: {
code: "MISSING_TABLE_NAME",
details: "테이블명 파라미터가 누락되었습니다.",
},
};
res.status(400).json(response);
return;
}
if (!Array.isArray(columnSettings) || columnSettings.length === 0) {
const response: ApiResponse<null> = {
success: false,
message: "컬럼 설정 목록이 필요합니다.",
error: {
code: "MISSING_COLUMN_SETTINGS",
details: "요청 본문에 컬럼 설정 목록이 누락되었습니다.",
},
};
res.status(400).json(response);
return;
}
2025-11-06 17:01:13 +09:00
if (!companyCode) {
logger.error(`회사 코드 누락 (일괄 업데이트): ${tableName}`, {
user: req.user,
hasUser: !!req.user,
userId: req.user?.userId,
companyCodeFromJWT: req.user?.companyCode,
settingsCount: columnSettings.length,
});
const response: ApiResponse<null> = {
success: false,
message: "회사 코드를 찾을 수 없습니다.",
error: {
code: "MISSING_COMPANY_CODE",
details:
"사용자 정보에서 회사 코드를 찾을 수 없습니다. 관리자에게 문의하세요.",
2025-11-06 17:01:13 +09:00
},
};
res.status(400).json(response);
return;
}
2025-09-01 11:00:38 +09:00
const tableManagementService = new TableManagementService();
await tableManagementService.updateAllColumnSettings(
tableName,
2025-11-06 17:01:13 +09:00
columnSettings,
companyCode // 🔥 회사 코드 전달
2025-09-01 11:00:38 +09:00
);
2025-08-25 14:08:08 +09:00
2025-09-01 11:00:38 +09:00
logger.info(
2025-11-06 17:01:13 +09:00
`전체 컬럼 설정 일괄 업데이트 완료: ${tableName}, ${columnSettings.length}개, company: ${companyCode}`
2025-09-01 11:00:38 +09:00
);
2025-08-25 14:08:08 +09:00
2025-09-01 11:00:38 +09:00
const response: ApiResponse<null> = {
success: true,
message: "모든 컬럼 설정을 성공적으로 저장했습니다.",
};
2025-08-25 14:08:08 +09:00
2025-09-01 11:00:38 +09:00
res.status(200).json(response);
2025-08-25 14:08:08 +09:00
} catch (error) {
logger.error("전체 컬럼 설정 일괄 업데이트 중 오류 발생:", error);
const response: ApiResponse<null> = {
success: false,
message: "컬럼 설정 저장 중 오류가 발생했습니다.",
error: {
code: "ALL_COLUMN_SETTINGS_UPDATE_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
};
res.status(500).json(response);
}
}
/**
*
*/
export async function getTableLabels(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName } = req.params;
logger.info(`=== 테이블 라벨 정보 조회 시작: ${tableName} ===`);
if (!tableName) {
const response: ApiResponse<null> = {
success: false,
message: "테이블명이 필요합니다.",
error: {
code: "MISSING_TABLE_NAME",
details: "테이블명 파라미터가 누락되었습니다.",
},
};
res.status(400).json(response);
return;
}
2025-09-01 11:00:38 +09:00
const tableManagementService = new TableManagementService();
const tableLabels = await tableManagementService.getTableLabels(tableName);
2025-08-25 14:08:08 +09:00
2025-09-01 11:00:38 +09:00
if (!tableLabels) {
2025-09-19 11:00:47 +09:00
// 라벨이 없으면 빈 객체를 성공으로 반환 (404 에러 대신)
const response: ApiResponse<{}> = {
success: true,
message: "테이블 라벨 정보를 조회했습니다.",
data: {},
2025-09-01 11:00:38 +09:00
};
2025-09-19 11:00:47 +09:00
res.status(200).json(response);
2025-09-01 11:00:38 +09:00
return;
2025-08-25 14:08:08 +09:00
}
2025-09-01 11:00:38 +09:00
logger.info(`테이블 라벨 정보 조회 완료: ${tableName}`);
const response: ApiResponse<any> = {
success: true,
message: "테이블 라벨 정보를 성공적으로 조회했습니다.",
data: tableLabels,
};
res.status(200).json(response);
2025-08-25 14:08:08 +09:00
} catch (error) {
logger.error("테이블 라벨 정보 조회 중 오류 발생:", error);
const response: ApiResponse<null> = {
success: false,
message: "테이블 라벨 정보 조회 중 오류가 발생했습니다.",
error: {
code: "TABLE_LABELS_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
};
res.status(500).json(response);
}
}
/**
*
*/
export async function getColumnLabels(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName, columnName } = req.params;
logger.info(`=== 컬럼 라벨 정보 조회 시작: ${tableName}.${columnName} ===`);
if (!tableName || !columnName) {
const response: ApiResponse<null> = {
success: false,
message: "테이블명과 컬럼명이 필요합니다.",
error: {
code: "MISSING_PARAMETERS",
details: "테이블명 또는 컬럼명 파라미터가 누락되었습니다.",
},
};
res.status(400).json(response);
return;
}
2025-09-01 11:00:38 +09:00
const tableManagementService = new TableManagementService();
const columnLabels = await tableManagementService.getColumnLabels(
tableName,
columnName
);
2025-08-25 14:08:08 +09:00
2025-09-01 11:00:38 +09:00
if (!columnLabels) {
2025-09-19 11:00:47 +09:00
// 라벨이 없으면 빈 객체를 성공으로 반환 (404 에러 대신)
const response: ApiResponse<{}> = {
success: true,
message: "컬럼 라벨 정보를 조회했습니다.",
data: {},
2025-09-01 11:00:38 +09:00
};
2025-09-19 11:00:47 +09:00
res.status(200).json(response);
2025-09-01 11:00:38 +09:00
return;
2025-08-25 14:08:08 +09:00
}
2025-09-01 11:00:38 +09:00
logger.info(`컬럼 라벨 정보 조회 완료: ${tableName}.${columnName}`);
const response: ApiResponse<any> = {
success: true,
message: "컬럼 라벨 정보를 성공적으로 조회했습니다.",
data: columnLabels,
};
res.status(200).json(response);
2025-08-25 14:08:08 +09:00
} catch (error) {
logger.error("컬럼 라벨 정보 조회 중 오류 발생:", error);
const response: ApiResponse<null> = {
success: false,
message: "컬럼 라벨 정보 조회 중 오류가 발생했습니다.",
error: {
code: "COLUMN_LABELS_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
};
res.status(500).json(response);
}
}
2025-09-01 11:48:12 +09:00
2025-09-08 14:20:01 +09:00
/**
*
*/
export async function updateTableLabel(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName } = req.params;
const { displayName, description } = req.body;
logger.info(`=== 테이블 라벨 설정 시작: ${tableName} ===`);
logger.info(`표시명: ${displayName}, 설명: ${description}`);
if (!tableName) {
const response: ApiResponse<null> = {
success: false,
message: "테이블명이 필요합니다.",
error: {
code: "MISSING_TABLE_NAME",
details: "테이블명 파라미터가 누락되었습니다.",
},
};
res.status(400).json(response);
return;
}
const tableManagementService = new TableManagementService();
await tableManagementService.updateTableLabel(
tableName,
displayName,
description
);
logger.info(`테이블 라벨 설정 완료: ${tableName}`);
const response: ApiResponse<null> = {
success: true,
message: "테이블 라벨이 성공적으로 설정되었습니다.",
data: null,
};
res.status(200).json(response);
} catch (error) {
logger.error("테이블 라벨 설정 중 오류 발생:", error);
const response: ApiResponse<null> = {
success: false,
message: "테이블 라벨 설정 중 오류가 발생했습니다.",
error: {
code: "TABLE_LABEL_UPDATE_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
};
res.status(500).json(response);
}
}
2025-09-01 11:48:12 +09:00
/**
2025-09-23 10:40:21 +09:00
*
2025-09-01 11:48:12 +09:00
*/
2025-09-23 10:40:21 +09:00
export async function updateColumnInputType(
2025-09-01 11:48:12 +09:00
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName, columnName } = req.params;
2025-09-23 10:40:21 +09:00
const { inputType, detailSettings } = req.body;
2025-11-06 17:01:13 +09:00
// 🔥 회사 코드 추출 (JWT에서 또는 DB에서 조회)
let companyCode = req.user?.companyCode;
2025-11-06 17:01:13 +09:00
if (!companyCode && req.user?.userId) {
// JWT에 없으면 DB에서 조회
const { query } = require("../database/db");
const userResult = await query(
`SELECT company_code FROM user_info WHERE user_id = $1`,
[req.user.userId]
);
companyCode = userResult[0]?.company_code;
logger.info(`DB에서 회사 코드 조회: ${req.user.userId}${companyCode}`);
}
2025-09-01 11:48:12 +09:00
logger.info(
2025-11-06 17:01:13 +09:00
`=== 컬럼 입력 타입 설정 시작: ${tableName}.${columnName} = ${inputType}, company: ${companyCode} ===`
2025-09-01 11:48:12 +09:00
);
2025-09-23 10:40:21 +09:00
if (!tableName || !columnName || !inputType) {
2025-09-01 11:48:12 +09:00
const response: ApiResponse<null> = {
success: false,
2025-09-23 10:40:21 +09:00
message: "테이블명, 컬럼명, 입력 타입이 모두 필요합니다.",
2025-09-01 11:48:12 +09:00
error: {
code: "MISSING_PARAMETERS",
details: "필수 파라미터가 누락되었습니다.",
},
};
res.status(400).json(response);
return;
}
2025-11-06 17:01:13 +09:00
if (!companyCode) {
logger.error(`회사 코드 누락 (입력 타입): ${tableName}.${columnName}`, {
user: req.user,
hasUser: !!req.user,
userId: req.user?.userId,
companyCodeFromJWT: req.user?.companyCode,
inputType,
});
const response: ApiResponse<null> = {
success: false,
message: "회사 코드를 찾을 수 없습니다.",
error: {
code: "MISSING_COMPANY_CODE",
details:
"사용자 정보에서 회사 코드를 찾을 수 없습니다. 관리자에게 문의하세요.",
2025-11-06 17:01:13 +09:00
},
};
res.status(400).json(response);
return;
}
2025-09-01 15:22:47 +09:00
const tableManagementService = new TableManagementService();
2025-09-23 10:40:21 +09:00
await tableManagementService.updateColumnInputType(
2025-09-01 15:22:47 +09:00
tableName,
columnName,
2025-09-23 10:40:21 +09:00
inputType,
2025-11-06 17:01:13 +09:00
companyCode,
2025-09-23 10:40:21 +09:00
detailSettings
2025-09-01 15:22:47 +09:00
);
2025-09-01 11:48:12 +09:00
2025-09-01 15:22:47 +09:00
logger.info(
2025-11-06 17:01:13 +09:00
`컬럼 입력 타입 설정 완료: ${tableName}.${columnName} = ${inputType}, company: ${companyCode}`
2025-09-01 15:22:47 +09:00
);
2025-09-01 11:48:12 +09:00
2025-09-01 15:22:47 +09:00
const response: ApiResponse<null> = {
success: true,
2025-09-23 10:40:21 +09:00
message: "컬럼 입력 타입이 성공적으로 설정되었습니다.",
2025-09-01 15:22:47 +09:00
data: null,
};
2025-09-01 11:48:12 +09:00
2025-09-01 15:22:47 +09:00
res.status(200).json(response);
2025-09-01 11:48:12 +09:00
} catch (error) {
2025-09-23 10:40:21 +09:00
logger.error("컬럼 입력 타입 설정 중 오류 발생:", error);
2025-09-01 11:48:12 +09:00
const response: ApiResponse<null> = {
success: false,
2025-09-23 10:40:21 +09:00
message: "컬럼 입력 타입 설정 중 오류가 발생했습니다.",
2025-09-01 11:48:12 +09:00
error: {
2025-09-23 10:40:21 +09:00
code: "INPUT_TYPE_UPDATE_ERROR",
2025-09-01 11:48:12 +09:00
details: error instanceof Error ? error.message : "Unknown error",
},
};
res.status(500).json(response);
}
}
2025-09-03 15:23:12 +09:00
/**
2025-11-04 14:33:39 +09:00
* ( )
*/
export async function getTableRecord(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName } = req.params;
const { filterColumn, filterValue, displayColumn } = req.body;
logger.info(`=== 단일 레코드 조회 시작: ${tableName} ===`);
logger.info(`필터: ${filterColumn} = ${filterValue}`);
logger.info(`표시 컬럼: ${displayColumn}`);
if (!tableName || !filterColumn || !filterValue || !displayColumn) {
const response: ApiResponse<null> = {
success: false,
message: "필수 파라미터가 누락되었습니다.",
error: {
code: "MISSING_PARAMETERS",
details:
"tableName, filterColumn, filterValue, displayColumn이 필요합니다.",
},
};
res.status(400).json(response);
return;
}
const tableManagementService = new TableManagementService();
// 단일 레코드 조회 (WHERE filterColumn = filterValue)
const result = await tableManagementService.getTableData(tableName, {
page: 1,
size: 1,
search: {
[filterColumn]: filterValue,
},
});
if (!result.data || result.data.length === 0) {
const response: ApiResponse<null> = {
success: false,
message: "데이터를 찾을 수 없습니다.",
error: {
code: "NOT_FOUND",
details: `${filterColumn} = ${filterValue}에 해당하는 데이터가 없습니다.`,
},
};
res.status(404).json(response);
return;
}
const record = result.data[0];
const displayValue = record[displayColumn];
logger.info(`레코드 조회 완료: ${displayColumn} = ${displayValue}`);
const response: ApiResponse<{ value: any; record: any }> = {
success: true,
message: "레코드를 성공적으로 조회했습니다.",
data: {
value: displayValue,
record: record,
},
};
res.status(200).json(response);
} catch (error) {
logger.error("레코드 조회 중 오류 발생:", error);
const response: ApiResponse<null> = {
success: false,
message: "레코드 조회 중 오류가 발생했습니다.",
error: {
code: "RECORD_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
};
res.status(500).json(response);
}
}
/**
* ( + + )
2025-09-03 15:23:12 +09:00
*/
export async function getTableData(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName } = req.params;
2025-09-03 16:38:10 +09:00
const {
page = 1,
size = 10,
search = {},
sortBy,
sortOrder = "asc",
2025-11-04 14:33:39 +09:00
autoFilter, // 🆕 자동 필터 설정 추가 (컴포넌트에서 직접 전달)
dataFilter, // 🆕 컬럼 값 기반 데이터 필터링
2025-09-03 16:38:10 +09:00
} = req.body;
2025-09-03 15:23:12 +09:00
logger.info(`=== 테이블 데이터 조회 시작: ${tableName} ===`);
logger.info(`페이징: page=${page}, size=${size}`);
logger.info(`검색 조건:`, search);
logger.info(`정렬: ${sortBy} ${sortOrder}`);
2025-11-04 14:33:39 +09:00
logger.info(`자동 필터:`, autoFilter); // 🆕
logger.info(`데이터 필터:`, dataFilter); // 🆕
2025-09-03 15:23:12 +09:00
if (!tableName) {
const response: ApiResponse<null> = {
success: false,
message: "테이블명이 필요합니다.",
error: {
code: "MISSING_TABLE_NAME",
details: "테이블명 파라미터가 누락되었습니다.",
},
};
res.status(400).json(response);
return;
}
const tableManagementService = new TableManagementService();
2025-09-03 16:38:10 +09:00
// 🆕 현재 사용자 필터 적용 (autoFilter가 없거나 enabled가 명시적으로 false가 아니면 기본 적용)
2025-11-04 14:33:39 +09:00
let enhancedSearch = { ...search };
const shouldApplyAutoFilter = autoFilter?.enabled !== false; // 기본값: true
if (shouldApplyAutoFilter && req.user) {
const filterColumn = autoFilter?.filterColumn || "company_code";
const userField = autoFilter?.userField || "companyCode";
2025-11-04 14:33:39 +09:00
const userValue = (req.user as any)[userField];
// 🆕 프리뷰용 회사 코드 오버라이드 (최고 관리자만 허용)
let finalCompanyCode = userValue;
if (autoFilter?.companyCodeOverride && userValue === "*") {
// 최고 관리자만 다른 회사 코드로 오버라이드 가능
finalCompanyCode = autoFilter.companyCodeOverride;
logger.info("🔓 최고 관리자 회사 코드 오버라이드:", {
originalCompanyCode: userValue,
overrideCompanyCode: autoFilter.companyCodeOverride,
tableName,
});
}
if (finalCompanyCode) {
enhancedSearch[filterColumn] = finalCompanyCode;
2025-11-04 14:33:39 +09:00
logger.info("🔍 현재 사용자 필터 적용:", {
filterColumn,
userField,
userValue: finalCompanyCode,
2025-11-04 14:33:39 +09:00
tableName,
});
} else {
logger.warn("⚠️ 사용자 정보 필드 값 없음:", {
userField,
user: req.user,
});
}
}
2025-09-03 15:23:12 +09:00
// 데이터 조회
2025-09-03 16:38:10 +09:00
const result = await tableManagementService.getTableData(tableName, {
page: parseInt(page),
size: parseInt(size),
2025-11-04 14:33:39 +09:00
search: enhancedSearch, // 🆕 필터가 적용된 search 사용
2025-09-03 16:38:10 +09:00
sortBy,
sortOrder,
dataFilter, // 🆕 데이터 필터 전달
2025-09-03 16:38:10 +09:00
});
2025-09-03 15:23:12 +09:00
2025-09-03 16:38:10 +09:00
logger.info(
`테이블 데이터 조회 완료: ${tableName}, 총 ${result.total}건, 페이지 ${result.page}/${result.totalPages}`
);
2025-09-03 15:23:12 +09:00
const response: ApiResponse<any> = {
success: true,
message: "테이블 데이터를 성공적으로 조회했습니다.",
data: result,
};
res.status(200).json(response);
} catch (error) {
logger.error("테이블 데이터 조회 중 오류 발생:", error);
const response: ApiResponse<null> = {
success: false,
message: "테이블 데이터 조회 중 오류가 발생했습니다.",
error: {
code: "TABLE_DATA_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
};
res.status(500).json(response);
}
}
2025-09-03 16:38:10 +09:00
/**
*
*/
export async function addTableData(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName } = req.params;
const data = req.body;
logger.info(`=== 테이블 데이터 추가 시작: ${tableName} ===`);
logger.info(`추가할 데이터:`, data);
if (!tableName) {
const response: ApiResponse<null> = {
success: false,
message: "테이블명이 필요합니다.",
error: {
code: "MISSING_TABLE_NAME",
details: "테이블명 파라미터가 누락되었습니다.",
},
};
res.status(400).json(response);
return;
}
if (!data || Object.keys(data).length === 0) {
const response: ApiResponse<null> = {
success: false,
message: "추가할 데이터가 필요합니다.",
error: {
code: "MISSING_DATA",
details: "요청 본문에 데이터가 없습니다.",
},
};
res.status(400).json(response);
return;
}
const tableManagementService = new TableManagementService();
// 🆕 멀티테넌시: company_code 자동 추가 (테이블에 company_code 컬럼이 있는 경우)
const companyCode = req.user?.companyCode;
if (companyCode && !data.company_code) {
// 테이블에 company_code 컬럼이 있는지 확인
const hasCompanyCodeColumn = await tableManagementService.hasColumn(tableName, "company_code");
if (hasCompanyCodeColumn) {
data.company_code = companyCode;
logger.info(`멀티테넌시: company_code 자동 추가 - ${companyCode}`);
}
}
// 🆕 writer 컬럼 자동 추가 (테이블에 writer 컬럼이 있고 값이 없는 경우)
const userId = req.user?.userId;
if (userId && !data.writer) {
const hasWriterColumn = await tableManagementService.hasColumn(tableName, "writer");
if (hasWriterColumn) {
data.writer = userId;
logger.info(`writer 자동 추가 - ${userId}`);
}
}
2025-09-03 16:38:10 +09:00
// 데이터 추가
await tableManagementService.addTableData(tableName, data);
2025-09-03 16:38:10 +09:00
logger.info(`테이블 데이터 추가 완료: ${tableName}`);
const response: ApiResponse<null> = {
2025-09-03 16:38:10 +09:00
success: true,
message: "테이블 데이터를 성공적으로 추가했습니다.",
2025-09-03 16:38:10 +09:00
};
res.status(201).json(response);
} catch (error) {
logger.error("테이블 데이터 추가 중 오류 발생:", error);
const response: ApiResponse<null> = {
success: false,
message: "테이블 데이터 추가 중 오류가 발생했습니다.",
error: {
code: "TABLE_ADD_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
};
res.status(500).json(response);
}
}
/**
*
*/
export async function editTableData(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName } = req.params;
const { originalData, updatedData } = req.body;
logger.info(`=== 테이블 데이터 수정 시작: ${tableName} ===`);
logger.info(`원본 데이터:`, originalData);
logger.info(`수정할 데이터:`, updatedData);
if (!tableName) {
const response: ApiResponse<null> = {
success: false,
message: "테이블명이 필요합니다.",
error: {
code: "INVALID_TABLE_NAME",
details: "테이블명이 누락되었습니다.",
},
};
res.status(400).json(response);
return;
}
if (!originalData || !updatedData) {
const response: ApiResponse<null> = {
success: false,
message: "원본 데이터와 수정할 데이터가 모두 필요합니다.",
error: {
code: "INVALID_DATA",
details: "originalData와 updatedData가 모두 제공되어야 합니다.",
},
};
res.status(400).json(response);
return;
}
if (Object.keys(updatedData).length === 0) {
const response: ApiResponse<null> = {
success: false,
message: "수정할 데이터가 없습니다.",
error: {
code: "INVALID_DATA",
details: "수정할 데이터가 비어있습니다.",
},
};
res.status(400).json(response);
return;
}
const tableManagementService = new TableManagementService();
// 데이터 수정
await tableManagementService.editTableData(
tableName,
originalData,
updatedData
);
logger.info(`테이블 데이터 수정 완료: ${tableName}`);
const response: ApiResponse<null> = {
success: true,
message: "테이블 데이터를 성공적으로 수정했습니다.",
};
res.status(200).json(response);
} catch (error) {
logger.error("테이블 데이터 수정 중 오류 발생:", error);
const response: ApiResponse<null> = {
success: false,
message: "테이블 데이터 수정 중 오류가 발생했습니다.",
error: {
code: "TABLE_EDIT_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
};
res.status(500).json(response);
}
}
/**
* ( )
*/
export async function getTableSchema(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName } = req.params;
logger.info(`=== 테이블 스키마 정보 조회 시작: ${tableName} ===`);
if (!tableName) {
const response: ApiResponse<null> = {
success: false,
message: "테이블명이 필요합니다.",
error: {
code: "MISSING_TABLE_NAME",
details: "테이블명 파라미터가 누락되었습니다.",
},
};
res.status(400).json(response);
return;
}
const tableManagementService = new TableManagementService();
const schema = await tableManagementService.getTableSchema(tableName);
logger.info(
`테이블 스키마 정보 조회 완료: ${tableName}, ${schema.length}개 컬럼`
);
const response: ApiResponse<ColumnTypeInfo[]> = {
success: true,
message: "테이블 스키마 정보를 성공적으로 조회했습니다.",
data: schema,
};
res.status(200).json(response);
} catch (error) {
logger.error("테이블 스키마 정보 조회 중 오류 발생:", error);
const response: ApiResponse<null> = {
success: false,
message: "테이블 스키마 정보 조회 중 오류가 발생했습니다.",
error: {
code: "TABLE_SCHEMA_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
};
res.status(500).json(response);
}
}
/**
*
*/
export async function checkTableExists(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName } = req.params;
logger.info(`=== 테이블 존재 여부 확인 시작: ${tableName} ===`);
if (!tableName) {
const response: ApiResponse<null> = {
success: false,
message: "테이블명이 필요합니다.",
error: {
code: "MISSING_TABLE_NAME",
details: "테이블명 파라미터가 누락되었습니다.",
},
};
res.status(400).json(response);
return;
}
const tableManagementService = new TableManagementService();
const exists = await tableManagementService.checkTableExists(tableName);
logger.info(`테이블 존재 여부 확인 완료: ${tableName} = ${exists}`);
const response: ApiResponse<{ exists: boolean }> = {
success: true,
message: "테이블 존재 여부를 확인했습니다.",
data: { exists },
};
res.status(200).json(response);
} catch (error) {
logger.error("테이블 존재 여부 확인 중 오류 발생:", error);
const response: ApiResponse<null> = {
success: false,
message: "테이블 존재 여부 확인 중 오류가 발생했습니다.",
error: {
code: "TABLE_EXISTS_CHECK_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
};
res.status(500).json(response);
}
}
/**
* ( )
*/
export async function getColumnWebTypes(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName } = req.params;
2025-11-06 17:01:13 +09:00
// 🔥 회사 코드 추출 (JWT에서 또는 DB에서 조회)
let companyCode = req.user?.companyCode;
2025-11-06 17:01:13 +09:00
if (!companyCode && req.user?.userId) {
// JWT에 없으면 DB에서 조회
const { query } = require("../database/db");
const userResult = await query(
`SELECT company_code FROM user_info WHERE user_id = $1`,
[req.user.userId]
);
companyCode = userResult[0]?.company_code;
logger.info(
`DB에서 회사 코드 조회 (조회): ${req.user.userId}${companyCode}`
);
2025-11-06 17:01:13 +09:00
}
logger.info(
`=== 컬럼 웹타입 정보 조회 시작: ${tableName}, company: ${companyCode} ===`
);
if (!tableName) {
const response: ApiResponse<null> = {
success: false,
message: "테이블명이 필요합니다.",
error: {
code: "MISSING_TABLE_NAME",
details: "테이블명 파라미터가 누락되었습니다.",
},
};
res.status(400).json(response);
return;
}
2025-11-06 17:01:13 +09:00
if (!companyCode) {
logger.error(`회사 코드 누락 (조회): ${tableName}`, {
user: req.user,
hasUser: !!req.user,
userId: req.user?.userId,
companyCodeFromJWT: req.user?.companyCode,
});
const response: ApiResponse<null> = {
success: false,
message: "회사 코드를 찾을 수 없습니다.",
error: {
code: "MISSING_COMPANY_CODE",
details:
"사용자 정보에서 회사 코드를 찾을 수 없습니다. 관리자에게 문의하세요.",
2025-11-06 17:01:13 +09:00
},
};
res.status(400).json(response);
return;
}
const tableManagementService = new TableManagementService();
2025-11-06 17:01:13 +09:00
const inputTypes = await tableManagementService.getColumnInputTypes(
tableName,
companyCode
);
logger.info(
2025-11-06 17:01:13 +09:00
`컬럼 입력타입 정보 조회 완료: ${tableName}, company: ${companyCode}, ${inputTypes.length}개 컬럼`
);
const response: ApiResponse<ColumnTypeInfo[]> = {
success: true,
2025-09-23 10:40:21 +09:00
message: "컬럼 입력타입 정보를 성공적으로 조회했습니다.",
data: inputTypes,
};
res.status(200).json(response);
} catch (error) {
logger.error("컬럼 웹타입 정보 조회 중 오류 발생:", error);
const response: ApiResponse<null> = {
success: false,
message: "컬럼 웹타입 정보 조회 중 오류가 발생했습니다.",
error: {
code: "COLUMN_WEB_TYPES_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
};
res.status(500).json(response);
}
}
/**
*
*/
export async function checkDatabaseConnection(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
logger.info("=== 데이터베이스 연결 상태 확인 시작 ===");
const tableManagementService = new TableManagementService();
const connectionStatus =
await tableManagementService.checkDatabaseConnection();
logger.info(
`데이터베이스 연결 상태: ${connectionStatus.connected ? "연결됨" : "연결 안됨"}`
);
const response: ApiResponse<{ connected: boolean; message: string }> = {
success: true,
message: "데이터베이스 연결 상태를 확인했습니다.",
data: connectionStatus,
};
res.status(200).json(response);
} catch (error) {
logger.error("데이터베이스 연결 상태 확인 중 오류 발생:", error);
const response: ApiResponse<null> = {
success: false,
message: "데이터베이스 연결 상태 확인 중 오류가 발생했습니다.",
error: {
code: "DATABASE_CONNECTION_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
};
res.status(500).json(response);
}
}
2025-09-03 16:38:10 +09:00
/**
*
*/
export async function deleteTableData(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName } = req.params;
const data = req.body;
logger.info(`=== 테이블 데이터 삭제 시작: ${tableName} ===`);
logger.info(`삭제할 데이터:`, data);
if (!tableName) {
const response: ApiResponse<null> = {
success: false,
message: "테이블명이 필요합니다.",
error: {
code: "MISSING_TABLE_NAME",
details: "테이블명 파라미터가 누락되었습니다.",
},
};
res.status(400).json(response);
return;
}
if (!data || (Array.isArray(data) && data.length === 0)) {
const response: ApiResponse<null> = {
success: false,
message: "삭제할 데이터가 필요합니다.",
error: {
code: "MISSING_DATA",
details: "요청 본문에 삭제할 데이터가 없습니다.",
},
};
res.status(400).json(response);
return;
}
const tableManagementService = new TableManagementService();
// 데이터 삭제
const deletedCount = await tableManagementService.deleteTableData(
tableName,
data
);
logger.info(
`테이블 데이터 삭제 완료: ${tableName}, ${deletedCount}건 삭제`
);
const response: ApiResponse<{ deletedCount: number }> = {
success: true,
message: `테이블 데이터를 성공적으로 삭제했습니다. (${deletedCount}건)`,
data: { deletedCount },
};
res.status(200).json(response);
} catch (error) {
logger.error("테이블 데이터 삭제 중 오류 발생:", error);
const response: ApiResponse<null> = {
success: false,
message: "테이블 데이터 삭제 중 오류가 발생했습니다.",
error: {
code: "TABLE_DELETE_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
};
res.status(500).json(response);
}
}
2025-09-23 10:40:21 +09:00
/**
* ( )
* @deprecated updateColumnInputType
*/
export async function updateColumnWebType(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName, columnName } = req.params;
const { webType, detailSettings, inputType } = req.body;
logger.warn(
`레거시 API 사용: updateColumnWebType → updateColumnInputType 사용 권장`
);
// webType을 inputType으로 변환
const convertedInputType = inputType || webType || "text";
// 새로운 메서드 호출
req.body = { inputType: convertedInputType, detailSettings };
await updateColumnInputType(req, res);
} catch (error) {
logger.error("레거시 컬럼 웹 타입 설정 중 오류 발생:", error);
const response: ApiResponse<null> = {
success: false,
message: "컬럼 웹 타입 설정 중 오류가 발생했습니다.",
error: {
code: "WEB_TYPE_UPDATE_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
};
res.status(500).json(response);
}
}
// ========================================
// 🎯 테이블 로그 시스템 API
// ========================================
/**
*
*/
export async function createLogTable(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName } = req.params;
const { pkColumn } = req.body;
const userId = req.user?.userId;
logger.info(`=== 로그 테이블 생성 시작: ${tableName} ===`);
if (!tableName) {
const response: ApiResponse<null> = {
success: false,
message: "테이블명이 필요합니다.",
error: {
code: "MISSING_TABLE_NAME",
details: "테이블명 파라미터가 누락되었습니다.",
},
};
res.status(400).json(response);
return;
}
if (!pkColumn || !pkColumn.columnName || !pkColumn.dataType) {
const response: ApiResponse<null> = {
success: false,
message: "PK 컬럼 정보가 필요합니다.",
error: {
code: "MISSING_PK_COLUMN",
details: "PK 컬럼명과 데이터 타입이 필요합니다.",
},
};
res.status(400).json(response);
return;
}
const tableManagementService = new TableManagementService();
await tableManagementService.createLogTable(tableName, pkColumn, userId);
logger.info(`로그 테이블 생성 완료: ${tableName}_log`);
const response: ApiResponse<null> = {
success: true,
message: "로그 테이블이 성공적으로 생성되었습니다.",
};
res.status(200).json(response);
} catch (error) {
logger.error("로그 테이블 생성 중 오류 발생:", error);
const response: ApiResponse<null> = {
success: false,
message: "로그 테이블 생성 중 오류가 발생했습니다.",
error: {
code: "LOG_TABLE_CREATE_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
};
res.status(500).json(response);
}
}
/**
*
*/
export async function getLogConfig(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName } = req.params;
logger.info(`=== 로그 설정 조회: ${tableName} ===`);
if (!tableName) {
const response: ApiResponse<null> = {
success: false,
message: "테이블명이 필요합니다.",
error: {
code: "MISSING_TABLE_NAME",
details: "테이블명 파라미터가 누락되었습니다.",
},
};
res.status(400).json(response);
return;
}
const tableManagementService = new TableManagementService();
const logConfig = await tableManagementService.getLogConfig(tableName);
const response: ApiResponse<typeof logConfig> = {
success: true,
message: "로그 설정을 조회했습니다.",
data: logConfig,
};
res.status(200).json(response);
} catch (error) {
logger.error("로그 설정 조회 중 오류 발생:", error);
const response: ApiResponse<null> = {
success: false,
message: "로그 설정 조회 중 오류가 발생했습니다.",
error: {
code: "LOG_CONFIG_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
};
res.status(500).json(response);
}
}
/**
*
*/
export async function getLogData(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName } = req.params;
const {
page = 1,
size = 20,
operationType,
startDate,
endDate,
changedBy,
originalId,
} = req.query;
logger.info(`=== 로그 데이터 조회: ${tableName} ===`);
if (!tableName) {
const response: ApiResponse<null> = {
success: false,
message: "테이블명이 필요합니다.",
error: {
code: "MISSING_TABLE_NAME",
details: "테이블명 파라미터가 누락되었습니다.",
},
};
res.status(400).json(response);
return;
}
const tableManagementService = new TableManagementService();
const result = await tableManagementService.getLogData(tableName, {
page: parseInt(page as string),
size: parseInt(size as string),
operationType: operationType as string,
startDate: startDate as string,
endDate: endDate as string,
changedBy: changedBy as string,
originalId: originalId as string,
});
2025-11-04 14:33:39 +09:00
logger.info(`로그 데이터 조회 완료: ${tableName}_log, ${result.total}`);
const response: ApiResponse<typeof result> = {
success: true,
message: "로그 데이터를 조회했습니다.",
data: result,
};
res.status(200).json(response);
} catch (error) {
logger.error("로그 데이터 조회 중 오류 발생:", error);
const response: ApiResponse<null> = {
success: false,
message: "로그 데이터 조회 중 오류가 발생했습니다.",
error: {
code: "LOG_DATA_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
};
res.status(500).json(response);
}
}
/**
* /
*/
export async function toggleLogTable(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName } = req.params;
const { isActive } = req.body;
2025-11-04 14:33:39 +09:00
logger.info(
`=== 로그 테이블 토글: ${tableName}, isActive: ${isActive} ===`
);
if (!tableName) {
const response: ApiResponse<null> = {
success: false,
message: "테이블명이 필요합니다.",
error: {
code: "MISSING_TABLE_NAME",
details: "테이블명 파라미터가 누락되었습니다.",
},
};
res.status(400).json(response);
return;
}
if (isActive === undefined || isActive === null) {
const response: ApiResponse<null> = {
success: false,
message: "isActive 값이 필요합니다.",
error: {
code: "MISSING_IS_ACTIVE",
details: "isActive 파라미터가 누락되었습니다.",
},
};
res.status(400).json(response);
return;
}
const tableManagementService = new TableManagementService();
await tableManagementService.toggleLogTable(
tableName,
isActive === "Y" || isActive === true
);
2025-11-04 14:33:39 +09:00
logger.info(`로그 테이블 토글 완료: ${tableName}, isActive: ${isActive}`);
const response: ApiResponse<null> = {
success: true,
message: `로그 기능이 ${isActive ? "활성화" : "비활성화"}되었습니다.`,
};
res.status(200).json(response);
} catch (error) {
logger.error("로그 테이블 토글 중 오류 발생:", error);
const response: ApiResponse<null> = {
success: false,
message: "로그 테이블 토글 중 오류가 발생했습니다.",
error: {
code: "LOG_TOGGLE_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
};
res.status(500).json(response);
}
}
/**
* ( )
*
* @route GET /api/table-management/category-columns
* @description table_type_columns에서 input_type = 'category'
*/
export async function getCategoryColumnsByCompany(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const companyCode = req.user?.companyCode;
logger.info("📥 회사별 카테고리 컬럼 조회 요청", { companyCode });
if (!companyCode) {
logger.error("❌ 회사 코드가 없습니다", { user: req.user });
res.status(400).json({
success: false,
message: "회사 코드를 확인할 수 없습니다. 다시 로그인해주세요.",
});
return;
}
const { getPool } = await import("../database/db");
const pool = getPool();
let columnsResult;
// 최고 관리자인 경우 company_code = '*'인 카테고리 컬럼 조회
if (companyCode === "*") {
const columnsQuery = `
SELECT DISTINCT
ttc.table_name AS "tableName",
COALESCE(
tl.table_label,
initcap(replace(ttc.table_name, '_', ' '))
) AS "tableLabel",
ttc.column_name AS "columnName",
COALESCE(
cl.column_label,
initcap(replace(ttc.column_name, '_', ' '))
) AS "columnLabel",
ttc.input_type AS "inputType"
FROM table_type_columns ttc
LEFT JOIN column_labels cl
ON ttc.table_name = cl.table_name
AND ttc.column_name = cl.column_name
LEFT JOIN table_labels tl
ON ttc.table_name = tl.table_name
WHERE ttc.input_type = 'category'
AND ttc.company_code = '*'
ORDER BY ttc.table_name, ttc.column_name
`;
columnsResult = await pool.query(columnsQuery);
logger.info("✅ 최고 관리자: 전체 카테고리 컬럼 조회 완료", {
rowCount: columnsResult.rows.length
});
} else {
// 일반 회사: 해당 회사의 카테고리 컬럼만 조회
const columnsQuery = `
SELECT DISTINCT
ttc.table_name AS "tableName",
COALESCE(
tl.table_label,
initcap(replace(ttc.table_name, '_', ' '))
) AS "tableLabel",
ttc.column_name AS "columnName",
COALESCE(
cl.column_label,
initcap(replace(ttc.column_name, '_', ' '))
) AS "columnLabel",
ttc.input_type AS "inputType"
FROM table_type_columns ttc
LEFT JOIN column_labels cl
ON ttc.table_name = cl.table_name
AND ttc.column_name = cl.column_name
LEFT JOIN table_labels tl
ON ttc.table_name = tl.table_name
WHERE ttc.input_type = 'category'
AND ttc.company_code = $1
ORDER BY ttc.table_name, ttc.column_name
`;
columnsResult = await pool.query(columnsQuery, [companyCode]);
logger.info("✅ 회사별 카테고리 컬럼 조회 완료", {
companyCode,
rowCount: columnsResult.rows.length
});
}
res.json({
success: true,
data: columnsResult.rows,
message: "카테고리 컬럼 조회 성공",
});
} catch (error: any) {
logger.error("❌ 회사별 카테고리 컬럼 조회 실패", { error: error.message });
res.status(500).json({
success: false,
message: "카테고리 컬럼 조회 중 오류가 발생했습니다.",
error: error.message,
});
}
}
/**
* ( )
*
* @route GET /api/table-management/menu/:menuObjid/category-columns
* @description category_column_mapping의
*
* :
* - 2 "고객사관리" discount_type, rounding_type
* - 3 "고객등록", "고객조회" ()
*/
export async function getCategoryColumnsByMenu(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { menuObjid } = req.params;
const companyCode = req.user?.companyCode;
logger.info("📥 메뉴별 카테고리 컬럼 조회 요청", { menuObjid, companyCode });
if (!menuObjid) {
res.status(400).json({
success: false,
message: "메뉴 OBJID가 필요합니다.",
});
return;
}
if (!companyCode) {
logger.error("❌ 회사 코드가 없습니다", { menuObjid, user: req.user });
res.status(400).json({
success: false,
message: "회사 코드를 확인할 수 없습니다. 다시 로그인해주세요.",
});
return;
}
const { getPool } = await import("../database/db");
const pool = getPool();
// 🆕 table_type_columns에서 직접 input_type = 'category'인 컬럼들을 조회
// category_column_mapping 대신 table_type_columns 기준으로 조회
logger.info("🔍 table_type_columns 기반 카테고리 컬럼 조회", { menuObjid, companyCode });
let columnsResult;
// 최고 관리자인 경우 모든 회사의 카테고리 컬럼 조회
if (companyCode === "*") {
const columnsQuery = `
SELECT DISTINCT
ttc.table_name AS "tableName",
COALESCE(
tl.table_label,
initcap(replace(ttc.table_name, '_', ' '))
) AS "tableLabel",
ttc.column_name AS "columnName",
COALESCE(
cl.column_label,
initcap(replace(ttc.column_name, '_', ' '))
) AS "columnLabel",
ttc.input_type AS "inputType"
FROM table_type_columns ttc
LEFT JOIN column_labels cl
ON ttc.table_name = cl.table_name
AND ttc.column_name = cl.column_name
LEFT JOIN table_labels tl
ON ttc.table_name = tl.table_name
WHERE ttc.input_type = 'category'
AND ttc.company_code = '*'
ORDER BY ttc.table_name, ttc.column_name
`;
columnsResult = await pool.query(columnsQuery);
logger.info("✅ 최고 관리자: 전체 카테고리 컬럼 조회 완료", {
rowCount: columnsResult.rows.length
});
} else {
// 일반 회사: 해당 회사의 카테고리 컬럼만 조회
const columnsQuery = `
SELECT DISTINCT
ttc.table_name AS "tableName",
COALESCE(
tl.table_label,
initcap(replace(ttc.table_name, '_', ' '))
) AS "tableLabel",
ttc.column_name AS "columnName",
COALESCE(
cl.column_label,
initcap(replace(ttc.column_name, '_', ' '))
) AS "columnLabel",
ttc.input_type AS "inputType"
FROM table_type_columns ttc
LEFT JOIN column_labels cl
ON ttc.table_name = cl.table_name
AND ttc.column_name = cl.column_name
LEFT JOIN table_labels tl
ON ttc.table_name = tl.table_name
WHERE ttc.input_type = 'category'
AND ttc.company_code = $1
ORDER BY ttc.table_name, ttc.column_name
`;
columnsResult = await pool.query(columnsQuery, [companyCode]);
logger.info("✅ 회사별 카테고리 컬럼 조회 완료", {
companyCode,
rowCount: columnsResult.rows.length
});
}
logger.info("✅ 카테고리 컬럼 조회 완료", {
columnCount: columnsResult.rows.length
});
res.json({
success: true,
data: columnsResult.rows,
message: "카테고리 컬럼 조회 성공",
});
} catch (error: any) {
logger.error("❌ 메뉴별 카테고리 컬럼 조회 실패");
logger.error("에러 메시지:", error.message);
logger.error("에러 스택:", error.stack);
logger.error("에러 전체:", error);
res.status(500).json({
success: false,
message: "카테고리 컬럼 조회에 실패했습니다.",
error: error.message,
stack: error.stack, // 디버깅용
});
}
}
/**
* API
*
* () .
*
* :
* {
* mainTable: { tableName: string, primaryKeyColumn: string },
* mainData: Record<string, any>,
* subTables: Array<{
* tableName: string,
* linkColumn: { mainField: string, subColumn: string },
* items: Record<string, any>[],
* options?: {
* saveMainAsFirst?: boolean,
* mainFieldMappings?: Array<{ formField: string, targetColumn: string }>,
* mainMarkerColumn?: string,
* mainMarkerValue?: any,
* subMarkerValue?: any,
* deleteExistingBefore?: boolean,
* }
* }>,
* isUpdate?: boolean
* }
*/
export async function multiTableSave(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
const pool = require("../database/db").getPool();
const client = await pool.connect();
try {
const { mainTable, mainData, subTables, isUpdate } = req.body;
const companyCode = req.user?.companyCode || "*";
logger.info("=== 다중 테이블 저장 시작 ===", {
mainTable,
mainDataKeys: Object.keys(mainData || {}),
subTablesCount: subTables?.length || 0,
isUpdate,
companyCode,
});
// 유효성 검사
if (!mainTable?.tableName || !mainTable?.primaryKeyColumn) {
res.status(400).json({
success: false,
message: "메인 테이블 설정이 올바르지 않습니다.",
});
return;
}
if (!mainData || Object.keys(mainData).length === 0) {
res.status(400).json({
success: false,
message: "저장할 메인 데이터가 없습니다.",
});
return;
}
await client.query("BEGIN");
// 1. 메인 테이블 저장
const mainTableName = mainTable.tableName;
const pkColumn = mainTable.primaryKeyColumn;
const pkValue = mainData[pkColumn];
// company_code 자동 추가 (최고 관리자가 아닌 경우)
if (companyCode !== "*" && !mainData.company_code) {
mainData.company_code = companyCode;
}
let mainResult: any;
if (isUpdate && pkValue) {
// UPDATE
const updateColumns = Object.keys(mainData)
.filter(col => col !== pkColumn)
.map((col, idx) => `"${col}" = $${idx + 1}`)
.join(", ");
const updateValues = Object.keys(mainData)
.filter(col => col !== pkColumn)
.map(col => mainData[col]);
// updated_at 컬럼 존재 여부 확인
const hasUpdatedAt = await client.query(`
SELECT 1 FROM information_schema.columns
WHERE table_name = $1 AND column_name = 'updated_at'
`, [mainTableName]);
const updatedAtClause = hasUpdatedAt.rowCount && hasUpdatedAt.rowCount > 0 ? ", updated_at = NOW()" : "";
const updateQuery = `
UPDATE "${mainTableName}"
SET ${updateColumns}${updatedAtClause}
WHERE "${pkColumn}" = $${updateValues.length + 1}
${companyCode !== "*" ? `AND company_code = $${updateValues.length + 2}` : ""}
RETURNING *
`;
const updateParams = companyCode !== "*"
? [...updateValues, pkValue, companyCode]
: [...updateValues, pkValue];
logger.info("메인 테이블 UPDATE:", { query: updateQuery, paramsCount: updateParams.length });
mainResult = await client.query(updateQuery, updateParams);
} else {
// INSERT
const columns = Object.keys(mainData).map(col => `"${col}"`).join(", ");
const placeholders = Object.keys(mainData).map((_, idx) => `$${idx + 1}`).join(", ");
const values = Object.values(mainData);
// updated_at 컬럼 존재 여부 확인
const hasUpdatedAt = await client.query(`
SELECT 1 FROM information_schema.columns
WHERE table_name = $1 AND column_name = 'updated_at'
`, [mainTableName]);
const updatedAtClause = hasUpdatedAt.rowCount && hasUpdatedAt.rowCount > 0 ? ", updated_at = NOW()" : "";
const updateSetClause = Object.keys(mainData)
.filter(col => col !== pkColumn)
.map(col => `"${col}" = EXCLUDED."${col}"`)
.join(", ");
const insertQuery = `
INSERT INTO "${mainTableName}" (${columns})
VALUES (${placeholders})
ON CONFLICT ("${pkColumn}") DO UPDATE SET
${updateSetClause}${updatedAtClause}
RETURNING *
`;
logger.info("메인 테이블 INSERT/UPSERT:", { query: insertQuery, paramsCount: values.length });
mainResult = await client.query(insertQuery, values);
}
if (mainResult.rowCount === 0) {
throw new Error("메인 테이블 저장 실패");
}
const savedMainData = mainResult.rows[0];
const savedPkValue = savedMainData[pkColumn];
logger.info("메인 테이블 저장 완료:", { pkColumn, savedPkValue });
// 2. 서브 테이블 저장
const subTableResults: any[] = [];
for (const subTableConfig of subTables || []) {
const { tableName, linkColumn, items, options } = subTableConfig;
// saveMainAsFirst가 활성화된 경우, items가 비어있어도 메인 데이터를 서브 테이블에 저장해야 함
const hasSaveMainAsFirst = options?.saveMainAsFirst &&
options?.mainFieldMappings &&
options.mainFieldMappings.length > 0;
if (!tableName || (!items?.length && !hasSaveMainAsFirst)) {
logger.info(`서브 테이블 ${tableName} 스킵: 데이터 없음 (saveMainAsFirst: ${hasSaveMainAsFirst})`);
continue;
}
logger.info(`서브 테이블 ${tableName} 저장 시작:`, {
itemsCount: items?.length || 0,
linkColumn,
options,
hasSaveMainAsFirst,
});
// 기존 데이터 삭제 옵션
if (options?.deleteExistingBefore && linkColumn?.subColumn) {
const deleteQuery = options?.deleteOnlySubItems && options?.mainMarkerColumn
? `DELETE FROM "${tableName}" WHERE "${linkColumn.subColumn}" = $1 AND "${options.mainMarkerColumn}" = $2`
: `DELETE FROM "${tableName}" WHERE "${linkColumn.subColumn}" = $1`;
const deleteParams = options?.deleteOnlySubItems && options?.mainMarkerColumn
? [savedPkValue, options.subMarkerValue ?? false]
: [savedPkValue];
logger.info(`서브 테이블 ${tableName} 기존 데이터 삭제:`, { deleteQuery, deleteParams });
await client.query(deleteQuery, deleteParams);
}
// 메인 데이터도 서브 테이블에 저장 (옵션)
// mainFieldMappings가 비어 있으면 건너뜀 (필수 컬럼 누락 방지)
logger.info(`saveMainAsFirst 옵션 확인:`, {
saveMainAsFirst: options?.saveMainAsFirst,
mainFieldMappings: options?.mainFieldMappings,
mainFieldMappingsLength: options?.mainFieldMappings?.length,
linkColumn,
mainDataKeys: Object.keys(mainData),
});
if (options?.saveMainAsFirst && options?.mainFieldMappings && options.mainFieldMappings.length > 0 && linkColumn?.subColumn) {
const mainSubItem: Record<string, any> = {
[linkColumn.subColumn]: savedPkValue,
};
// 메인 필드 매핑 적용
for (const mapping of options.mainFieldMappings) {
if (mapping.formField && mapping.targetColumn) {
mainSubItem[mapping.targetColumn] = mainData[mapping.formField];
}
}
// 메인 마커 설정
if (options.mainMarkerColumn) {
mainSubItem[options.mainMarkerColumn] = options.mainMarkerValue ?? true;
}
// company_code 추가
if (companyCode !== "*") {
mainSubItem.company_code = companyCode;
}
// 먼저 기존 데이터 존재 여부 확인 (user_id + is_primary 조합)
const checkQuery = `
SELECT * FROM "${tableName}"
WHERE "${linkColumn.subColumn}" = $1
${options.mainMarkerColumn ? `AND "${options.mainMarkerColumn}" = $2` : ""}
${companyCode !== "*" ? `AND company_code = $${options.mainMarkerColumn ? 3 : 2}` : ""}
LIMIT 1
`;
const checkParams: any[] = [savedPkValue];
if (options.mainMarkerColumn) {
checkParams.push(options.mainMarkerValue ?? true);
}
if (companyCode !== "*") {
checkParams.push(companyCode);
}
const existingResult = await client.query(checkQuery, checkParams);
if (existingResult.rows.length > 0) {
// UPDATE
const updateColumns = Object.keys(mainSubItem)
.filter(col => col !== linkColumn.subColumn && col !== options.mainMarkerColumn && col !== "company_code")
.map((col, idx) => `"${col}" = $${idx + 1}`)
.join(", ");
const updateValues = Object.keys(mainSubItem)
.filter(col => col !== linkColumn.subColumn && col !== options.mainMarkerColumn && col !== "company_code")
.map(col => mainSubItem[col]);
if (updateColumns) {
const updateQuery = `
UPDATE "${tableName}"
SET ${updateColumns}
WHERE "${linkColumn.subColumn}" = $${updateValues.length + 1}
${options.mainMarkerColumn ? `AND "${options.mainMarkerColumn}" = $${updateValues.length + 2}` : ""}
${companyCode !== "*" ? `AND company_code = $${updateValues.length + (options.mainMarkerColumn ? 3 : 2)}` : ""}
RETURNING *
`;
const updateParams = [...updateValues, savedPkValue];
if (options.mainMarkerColumn) {
updateParams.push(options.mainMarkerValue ?? true);
}
if (companyCode !== "*") {
updateParams.push(companyCode);
}
const updateResult = await client.query(updateQuery, updateParams);
subTableResults.push({ tableName, type: "main", data: updateResult.rows[0] });
} else {
subTableResults.push({ tableName, type: "main", data: existingResult.rows[0] });
}
} else {
// INSERT
const mainSubColumns = Object.keys(mainSubItem).map(col => `"${col}"`).join(", ");
const mainSubPlaceholders = Object.keys(mainSubItem).map((_, idx) => `$${idx + 1}`).join(", ");
const mainSubValues = Object.values(mainSubItem);
const insertQuery = `
INSERT INTO "${tableName}" (${mainSubColumns})
VALUES (${mainSubPlaceholders})
RETURNING *
`;
const insertResult = await client.query(insertQuery, mainSubValues);
subTableResults.push({ tableName, type: "main", data: insertResult.rows[0] });
}
}
// 서브 아이템들 저장
for (const item of items) {
// 연결 컬럼 값 설정
if (linkColumn?.subColumn) {
item[linkColumn.subColumn] = savedPkValue;
}
// company_code 추가
if (companyCode !== "*" && !item.company_code) {
item.company_code = companyCode;
}
const subColumns = Object.keys(item).map(col => `"${col}"`).join(", ");
const subPlaceholders = Object.keys(item).map((_, idx) => `$${idx + 1}`).join(", ");
const subValues = Object.values(item);
const subInsertQuery = `
INSERT INTO "${tableName}" (${subColumns})
VALUES (${subPlaceholders})
RETURNING *
`;
logger.info(`서브 테이블 ${tableName} 아이템 저장:`, { subInsertQuery, subValuesCount: subValues.length });
const subResult = await client.query(subInsertQuery, subValues);
subTableResults.push({ tableName, type: "sub", data: subResult.rows[0] });
}
logger.info(`서브 테이블 ${tableName} 저장 완료`);
}
await client.query("COMMIT");
logger.info("=== 다중 테이블 저장 완료 ===", {
mainTable: mainTableName,
mainPk: savedPkValue,
subTableResultsCount: subTableResults.length,
});
res.json({
success: true,
message: "다중 테이블 저장이 완료되었습니다.",
data: {
main: savedMainData,
subTables: subTableResults,
},
});
} catch (error: any) {
await client.query("ROLLBACK");
logger.error("다중 테이블 저장 실패:", {
message: error.message,
stack: error.stack,
});
res.status(500).json({
success: false,
message: error.message || "다중 테이블 저장에 실패했습니다.",
error: error.message,
});
} finally {
client.release();
}
}
2026-01-08 15:56:06 +09:00
/**
*
* column_labels의 entity/category
2026-01-08 15:56:06 +09:00
*/
export async function getTableEntityRelations(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { leftTable, rightTable } = req.query;
if (!leftTable || !rightTable) {
res.status(400).json({
2026-01-08 15:56:06 +09:00
success: false,
message: "leftTable과 rightTable 파라미터가 필요합니다.",
});
2026-01-08 15:56:06 +09:00
return;
}
logger.info("=== 테이블 엔티티 관계 조회 ===", { leftTable, rightTable });
// 두 테이블의 컬럼 라벨 정보 조회
const columnLabelsQuery = `
SELECT
table_name,
column_name,
column_label,
web_type,
detail_settings
FROM column_labels
WHERE table_name IN ($1, $2)
AND web_type IN ('entity', 'category')
`;
const result = await query(columnLabelsQuery, [leftTable, rightTable]);
// 관계 분석
const relations: Array<{
fromTable: string;
fromColumn: string;
toTable: string;
toColumn: string;
relationType: string;
}> = [];
for (const row of result) {
try {
const detailSettings = typeof row.detail_settings === "string"
? JSON.parse(row.detail_settings)
: row.detail_settings;
if (detailSettings && detailSettings.referenceTable) {
const refTable = detailSettings.referenceTable;
const refColumn = detailSettings.referenceColumn || "id";
// leftTable과 rightTable 간의 관계인지 확인
if (
(row.table_name === leftTable && refTable === rightTable) ||
(row.table_name === rightTable && refTable === leftTable)
) {
relations.push({
fromTable: row.table_name,
fromColumn: row.column_name,
toTable: refTable,
toColumn: refColumn,
relationType: row.web_type,
});
}
}
} catch (parseError) {
logger.warn("detail_settings 파싱 오류:", {
table: row.table_name,
column: row.column_name,
error: parseError
});
}
}
2026-01-08 15:56:06 +09:00
logger.info("테이블 엔티티 관계 조회 완료", {
leftTable,
rightTable,
relationsCount: relations.length
});
2026-01-08 15:56:06 +09:00
res.json({
2026-01-08 15:56:06 +09:00
success: true,
data: {
leftTable,
rightTable,
2026-01-08 15:56:06 +09:00
relations,
},
});
} catch (error: any) {
logger.error("테이블 엔티티 관계 조회 실패:", error);
res.status(500).json({
2026-01-08 15:56:06 +09:00
success: false,
message: "테이블 엔티티 관계 조회에 실패했습니다.",
error: error.message,
});
2026-01-08 15:56:06 +09:00
}
}
2026-01-15 15:17:52 +09:00
/**
* (FK로 )
* GET /api/table-management/columns/:tableName/referenced-by
*
* column_labels에서 reference_table이
* FK .
*/
export async function getReferencedByTables(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName } = req.params;
logger.info(
`=== 테이블 참조 관계 조회 시작: ${tableName} 을 참조하는 테이블 ===`
);
if (!tableName) {
const response: ApiResponse<null> = {
success: false,
message: "tableName 파라미터가 필요합니다.",
error: {
code: "MISSING_PARAMETERS",
details: "tableName 경로 파라미터가 필요합니다.",
},
};
res.status(400).json(response);
return;
}
// column_labels에서 reference_table이 현재 테이블인 레코드 조회
// input_type이 'entity'인 것만 조회 (실제 FK 관계)
const sqlQuery = `
SELECT DISTINCT
cl.table_name,
cl.column_name,
cl.column_label,
cl.reference_table,
cl.reference_column,
cl.display_column,
cl.table_name as table_label
FROM column_labels cl
WHERE cl.reference_table = $1
AND cl.input_type = 'entity'
ORDER BY cl.table_name, cl.column_name
`;
const result = await query(sqlQuery, [tableName]);
const referencedByTables = result.map((row: any) => ({
tableName: row.table_name,
tableLabel: row.table_label,
columnName: row.column_name,
columnLabel: row.column_label,
referenceTable: row.reference_table,
referenceColumn: row.reference_column || "id",
displayColumn: row.display_column,
}));
logger.info(
`테이블 참조 관계 조회 완료: ${referencedByTables.length}개 발견`
);
const response: ApiResponse<any> = {
success: true,
message: `${referencedByTables.length}개의 테이블이 ${tableName}을 참조합니다.`,
data: referencedByTables,
};
res.status(200).json(response);
} catch (error) {
logger.error("테이블 참조 관계 조회 중 오류 발생:", error);
const response: ApiResponse<null> = {
success: false,
message: "테이블 참조 관계 조회 중 오류가 발생했습니다.",
error: {
code: "REFERENCED_BY_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
};
res.status(500).json(response);
}
}