2188 lines
67 KiB
TypeScript
2188 lines
67 KiB
TypeScript
import { Request, Response } from "express";
|
|
import { Client } from "pg";
|
|
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";
|
|
import { query } from "../database/db"; // 🆕 query 함수 import
|
|
|
|
/**
|
|
* 테이블 목록 조회
|
|
*/
|
|
export async function getTableList(
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<void> {
|
|
try {
|
|
logger.info("=== 테이블 목록 조회 시작 ===");
|
|
|
|
const tableManagementService = new TableManagementService();
|
|
const tableList = await tableManagementService.getTableList();
|
|
|
|
logger.info(`테이블 목록 조회 결과: ${tableList.length}개`);
|
|
|
|
const response: ApiResponse<TableInfo[]> = {
|
|
success: true,
|
|
message: "테이블 목록을 성공적으로 조회했습니다.",
|
|
data: tableList,
|
|
};
|
|
|
|
res.status(200).json(response);
|
|
} 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;
|
|
const { page = 1, size = 50 } = req.query;
|
|
|
|
// 🔥 회사 코드 추출 (JWT에서 또는 DB에서 조회)
|
|
let companyCode = req.user?.companyCode;
|
|
|
|
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}`
|
|
);
|
|
}
|
|
|
|
logger.info(
|
|
`=== 컬럼 정보 조회 시작: ${tableName} (page: ${page}, size: ${size}), company: ${companyCode} ===`
|
|
);
|
|
|
|
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.getColumnList(
|
|
tableName,
|
|
parseInt(page as string),
|
|
parseInt(size as string),
|
|
companyCode // 🔥 회사 코드 전달
|
|
);
|
|
|
|
logger.info(
|
|
`컬럼 정보 조회 결과: ${tableName}, ${result.columns.length}/${result.total}개 (${result.page}/${result.totalPages} 페이지)`
|
|
);
|
|
|
|
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: "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;
|
|
|
|
// 🔥 회사 코드 추출 (JWT에서 또는 DB에서 조회)
|
|
let companyCode = req.user?.companyCode;
|
|
|
|
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}`);
|
|
}
|
|
|
|
logger.info(
|
|
`=== 컬럼 설정 업데이트 시작: ${tableName}.${columnName}, company: ${companyCode} ===`
|
|
);
|
|
|
|
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;
|
|
}
|
|
|
|
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:
|
|
"사용자 정보에서 회사 코드를 찾을 수 없습니다. 관리자에게 문의하세요.",
|
|
},
|
|
};
|
|
res.status(400).json(response);
|
|
return;
|
|
}
|
|
|
|
const tableManagementService = new TableManagementService();
|
|
await tableManagementService.updateColumnSettings(
|
|
tableName,
|
|
columnName,
|
|
settings,
|
|
companyCode // 🔥 회사 코드 전달
|
|
);
|
|
|
|
logger.info(
|
|
`컬럼 설정 업데이트 완료: ${tableName}.${columnName}, company: ${companyCode}`
|
|
);
|
|
|
|
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: "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;
|
|
|
|
// 🔥 회사 코드 추출 (JWT에서 또는 DB에서 조회)
|
|
let companyCode = req.user?.companyCode;
|
|
|
|
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}`);
|
|
}
|
|
|
|
// 🔍 디버깅: 사용자 정보 출력
|
|
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} ===`
|
|
);
|
|
|
|
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;
|
|
}
|
|
|
|
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:
|
|
"사용자 정보에서 회사 코드를 찾을 수 없습니다. 관리자에게 문의하세요.",
|
|
},
|
|
};
|
|
res.status(400).json(response);
|
|
return;
|
|
}
|
|
|
|
const tableManagementService = new TableManagementService();
|
|
await tableManagementService.updateAllColumnSettings(
|
|
tableName,
|
|
columnSettings,
|
|
companyCode // 🔥 회사 코드 전달
|
|
);
|
|
|
|
logger.info(
|
|
`전체 컬럼 설정 일괄 업데이트 완료: ${tableName}, ${columnSettings.length}개, company: ${companyCode}`
|
|
);
|
|
|
|
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: "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;
|
|
}
|
|
|
|
const tableManagementService = new TableManagementService();
|
|
const tableLabels = await tableManagementService.getTableLabels(tableName);
|
|
|
|
if (!tableLabels) {
|
|
// 라벨이 없으면 빈 객체를 성공으로 반환 (404 에러 대신)
|
|
const response: ApiResponse<{}> = {
|
|
success: true,
|
|
message: "테이블 라벨 정보를 조회했습니다.",
|
|
data: {},
|
|
};
|
|
res.status(200).json(response);
|
|
return;
|
|
}
|
|
|
|
logger.info(`테이블 라벨 정보 조회 완료: ${tableName}`);
|
|
|
|
const response: ApiResponse<any> = {
|
|
success: true,
|
|
message: "테이블 라벨 정보를 성공적으로 조회했습니다.",
|
|
data: tableLabels,
|
|
};
|
|
|
|
res.status(200).json(response);
|
|
} 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;
|
|
}
|
|
|
|
const tableManagementService = new TableManagementService();
|
|
const columnLabels = await tableManagementService.getColumnLabels(
|
|
tableName,
|
|
columnName
|
|
);
|
|
|
|
if (!columnLabels) {
|
|
// 라벨이 없으면 빈 객체를 성공으로 반환 (404 에러 대신)
|
|
const response: ApiResponse<{}> = {
|
|
success: true,
|
|
message: "컬럼 라벨 정보를 조회했습니다.",
|
|
data: {},
|
|
};
|
|
res.status(200).json(response);
|
|
return;
|
|
}
|
|
|
|
logger.info(`컬럼 라벨 정보 조회 완료: ${tableName}.${columnName}`);
|
|
|
|
const response: ApiResponse<any> = {
|
|
success: true,
|
|
message: "컬럼 라벨 정보를 성공적으로 조회했습니다.",
|
|
data: columnLabels,
|
|
};
|
|
|
|
res.status(200).json(response);
|
|
} 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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 테이블 라벨 설정
|
|
*/
|
|
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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 컬럼 입력 타입 설정
|
|
*/
|
|
export async function updateColumnInputType(
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<void> {
|
|
try {
|
|
const { tableName, columnName } = req.params;
|
|
const { inputType, detailSettings } = req.body;
|
|
|
|
// 🔥 회사 코드 추출 (JWT에서 또는 DB에서 조회)
|
|
let companyCode = req.user?.companyCode;
|
|
|
|
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}`);
|
|
}
|
|
|
|
logger.info(
|
|
`=== 컬럼 입력 타입 설정 시작: ${tableName}.${columnName} = ${inputType}, company: ${companyCode} ===`
|
|
);
|
|
|
|
if (!tableName || !columnName || !inputType) {
|
|
const response: ApiResponse<null> = {
|
|
success: false,
|
|
message: "테이블명, 컬럼명, 입력 타입이 모두 필요합니다.",
|
|
error: {
|
|
code: "MISSING_PARAMETERS",
|
|
details: "필수 파라미터가 누락되었습니다.",
|
|
},
|
|
};
|
|
res.status(400).json(response);
|
|
return;
|
|
}
|
|
|
|
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:
|
|
"사용자 정보에서 회사 코드를 찾을 수 없습니다. 관리자에게 문의하세요.",
|
|
},
|
|
};
|
|
res.status(400).json(response);
|
|
return;
|
|
}
|
|
|
|
const tableManagementService = new TableManagementService();
|
|
await tableManagementService.updateColumnInputType(
|
|
tableName,
|
|
columnName,
|
|
inputType,
|
|
companyCode,
|
|
detailSettings
|
|
);
|
|
|
|
logger.info(
|
|
`컬럼 입력 타입 설정 완료: ${tableName}.${columnName} = ${inputType}, company: ${companyCode}`
|
|
);
|
|
|
|
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: "INPUT_TYPE_UPDATE_ERROR",
|
|
details: error instanceof Error ? error.message : "Unknown error",
|
|
},
|
|
};
|
|
|
|
res.status(500).json(response);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 단일 레코드 조회 (자동 입력용)
|
|
*/
|
|
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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 테이블 데이터 조회 (페이징 + 검색 + 필터링)
|
|
*/
|
|
export async function getTableData(
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<void> {
|
|
try {
|
|
const { tableName } = req.params;
|
|
const {
|
|
page = 1,
|
|
size = 10,
|
|
search = {},
|
|
sortBy,
|
|
sortOrder = "asc",
|
|
autoFilter, // 🆕 자동 필터 설정 추가 (컴포넌트에서 직접 전달)
|
|
dataFilter, // 🆕 컬럼 값 기반 데이터 필터링
|
|
} = req.body;
|
|
|
|
logger.info(`=== 테이블 데이터 조회 시작: ${tableName} ===`);
|
|
logger.info(`페이징: page=${page}, size=${size}`);
|
|
logger.info(`검색 조건:`, search);
|
|
logger.info(`정렬: ${sortBy} ${sortOrder}`);
|
|
logger.info(`자동 필터:`, autoFilter); // 🆕
|
|
logger.info(`데이터 필터:`, dataFilter); // 🆕
|
|
|
|
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();
|
|
|
|
// 🆕 현재 사용자 필터 적용 (autoFilter가 없거나 enabled가 명시적으로 false가 아니면 기본 적용)
|
|
let enhancedSearch = { ...search };
|
|
const shouldApplyAutoFilter = autoFilter?.enabled !== false; // 기본값: true
|
|
if (shouldApplyAutoFilter && req.user) {
|
|
const filterColumn = autoFilter?.filterColumn || "company_code";
|
|
const userField = autoFilter?.userField || "companyCode";
|
|
const userValue = (req.user as any)[userField];
|
|
|
|
// 🆕 최고 관리자(company_code = '*')는 모든 회사 데이터 조회 가능
|
|
if (userValue && userValue !== "*") {
|
|
enhancedSearch[filterColumn] = userValue;
|
|
|
|
logger.info("🔍 현재 사용자 필터 적용:", {
|
|
filterColumn,
|
|
userField,
|
|
userValue,
|
|
tableName,
|
|
});
|
|
} else if (userValue === "*") {
|
|
logger.info("🔓 최고 관리자 - 회사 필터 미적용 (모든 회사 데이터 조회)", {
|
|
tableName,
|
|
});
|
|
} else {
|
|
logger.warn("⚠️ 사용자 정보 필드 값 없음:", {
|
|
userField,
|
|
user: req.user,
|
|
});
|
|
}
|
|
}
|
|
|
|
// 🆕 최종 검색 조건 로그
|
|
logger.info(`🔍 최종 검색 조건 (enhancedSearch):`, JSON.stringify(enhancedSearch));
|
|
|
|
// 데이터 조회
|
|
const result = await tableManagementService.getTableData(tableName, {
|
|
page: parseInt(page),
|
|
size: parseInt(size),
|
|
search: enhancedSearch, // 🆕 필터가 적용된 search 사용
|
|
sortBy,
|
|
sortOrder,
|
|
dataFilter, // 🆕 데이터 필터 전달
|
|
});
|
|
|
|
logger.info(
|
|
`테이블 데이터 조회 완료: ${tableName}, 총 ${result.total}건, 페이지 ${result.page}/${result.totalPages}`
|
|
);
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 테이블 데이터 추가
|
|
*/
|
|
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}`);
|
|
}
|
|
}
|
|
|
|
// 데이터 추가
|
|
const result = await tableManagementService.addTableData(tableName, data);
|
|
|
|
logger.info(`테이블 데이터 추가 완료: ${tableName}`);
|
|
|
|
// 무시된 컬럼이 있으면 경고 정보 포함
|
|
const response: ApiResponse<{
|
|
skippedColumns?: string[];
|
|
savedColumns?: string[];
|
|
}> = {
|
|
success: true,
|
|
message: result.skippedColumns.length > 0
|
|
? `테이블 데이터를 추가했습니다. (무시된 컬럼 ${result.skippedColumns.length}개: ${result.skippedColumns.join(", ")})`
|
|
: "테이블 데이터를 성공적으로 추가했습니다.",
|
|
data: {
|
|
skippedColumns: result.skippedColumns.length > 0 ? result.skippedColumns : undefined,
|
|
savedColumns: result.savedColumns,
|
|
},
|
|
};
|
|
|
|
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;
|
|
|
|
// 🔥 회사 코드 추출 (JWT에서 또는 DB에서 조회)
|
|
let companyCode = req.user?.companyCode;
|
|
|
|
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}`
|
|
);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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:
|
|
"사용자 정보에서 회사 코드를 찾을 수 없습니다. 관리자에게 문의하세요.",
|
|
},
|
|
};
|
|
res.status(400).json(response);
|
|
return;
|
|
}
|
|
|
|
const tableManagementService = new TableManagementService();
|
|
const inputTypes = await tableManagementService.getColumnInputTypes(
|
|
tableName,
|
|
companyCode
|
|
);
|
|
|
|
logger.info(
|
|
`컬럼 입력타입 정보 조회 완료: ${tableName}, company: ${companyCode}, ${inputTypes.length}개 컬럼`
|
|
);
|
|
|
|
const response: ApiResponse<ColumnTypeInfo[]> = {
|
|
success: true,
|
|
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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 테이블 데이터 삭제
|
|
*/
|
|
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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 컬럼 웹 타입 설정 (레거시 지원)
|
|
* @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,
|
|
});
|
|
|
|
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;
|
|
|
|
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
|
|
);
|
|
|
|
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/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;
|
|
}
|
|
|
|
const { getPool } = await import("../database/db");
|
|
const pool = getPool();
|
|
|
|
// 1. category_column_mapping 테이블 존재 여부 확인
|
|
const tableExistsResult = await pool.query(`
|
|
SELECT EXISTS (
|
|
SELECT FROM information_schema.tables
|
|
WHERE table_name = 'category_column_mapping'
|
|
) as table_exists
|
|
`);
|
|
const mappingTableExists = tableExistsResult.rows[0]?.table_exists === true;
|
|
|
|
let columnsResult;
|
|
|
|
if (mappingTableExists) {
|
|
// 🆕 category_column_mapping을 사용한 계층 구조 기반 조회
|
|
logger.info("🔍 category_column_mapping 기반 카테고리 컬럼 조회 (계층 구조 상속)", { menuObjid, companyCode });
|
|
|
|
// 현재 메뉴와 모든 상위 메뉴의 objid 조회 (재귀)
|
|
const ancestorMenuQuery = `
|
|
WITH RECURSIVE menu_hierarchy AS (
|
|
-- 현재 메뉴
|
|
SELECT objid, parent_obj_id, menu_type, menu_name_kor
|
|
FROM menu_info
|
|
WHERE objid = $1
|
|
|
|
UNION ALL
|
|
|
|
-- 부모 메뉴 재귀 조회
|
|
SELECT m.objid, m.parent_obj_id, m.menu_type, m.menu_name_kor
|
|
FROM menu_info m
|
|
INNER JOIN menu_hierarchy mh ON m.objid = mh.parent_obj_id
|
|
WHERE m.parent_obj_id != 0 -- 최상위 메뉴(parent_obj_id=0) 제외
|
|
)
|
|
SELECT
|
|
ARRAY_AGG(objid) as menu_objids,
|
|
ARRAY_AGG(menu_name_kor) as menu_names
|
|
FROM menu_hierarchy
|
|
`;
|
|
|
|
const ancestorMenuResult = await pool.query(ancestorMenuQuery, [parseInt(menuObjid)]);
|
|
const ancestorMenuObjids = ancestorMenuResult.rows[0]?.menu_objids || [parseInt(menuObjid)];
|
|
const ancestorMenuNames = ancestorMenuResult.rows[0]?.menu_names || [];
|
|
|
|
logger.info("✅ 상위 메뉴 계층 조회 완료", {
|
|
ancestorMenuObjids,
|
|
ancestorMenuNames,
|
|
hierarchyDepth: ancestorMenuObjids.length
|
|
});
|
|
|
|
// 상위 메뉴들에 설정된 모든 카테고리 컬럼 조회 (테이블 필터링 제거)
|
|
const columnsQuery = `
|
|
SELECT DISTINCT
|
|
ttc.table_name AS "tableName",
|
|
COALESCE(
|
|
tl.table_label,
|
|
initcap(replace(ttc.table_name, '_', ' '))
|
|
) AS "tableLabel",
|
|
ccm.logical_column_name AS "columnName",
|
|
COALESCE(
|
|
cl.column_label,
|
|
initcap(replace(ccm.logical_column_name, '_', ' '))
|
|
) AS "columnLabel",
|
|
ttc.input_type AS "inputType",
|
|
ccm.menu_objid AS "definedAtMenuObjid"
|
|
FROM category_column_mapping ccm
|
|
INNER JOIN table_type_columns ttc
|
|
ON ccm.table_name = ttc.table_name
|
|
AND ccm.physical_column_name = ttc.column_name
|
|
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 ccm.company_code = $1
|
|
AND ccm.menu_objid = ANY($2)
|
|
AND ttc.input_type = 'category'
|
|
ORDER BY ttc.table_name, ccm.logical_column_name
|
|
`;
|
|
|
|
columnsResult = await pool.query(columnsQuery, [companyCode, ancestorMenuObjids]);
|
|
logger.info("✅ category_column_mapping 기반 조회 완료 (계층 구조 상속)", {
|
|
rowCount: columnsResult.rows.length,
|
|
columns: columnsResult.rows.map((r: any) => `${r.tableName}.${r.columnName}`)
|
|
});
|
|
} else {
|
|
// 🔄 레거시 방식: 형제 메뉴들의 테이블에서 모든 카테고리 컬럼 조회
|
|
logger.info("🔍 레거시 방식: 형제 메뉴 테이블 기반 카테고리 컬럼 조회", { menuObjid, companyCode });
|
|
|
|
// 형제 메뉴 조회
|
|
const { getSiblingMenuObjids } = await import("../services/menuService");
|
|
const siblingObjids = await getSiblingMenuObjids(parseInt(menuObjid));
|
|
|
|
// 형제 메뉴들이 사용하는 테이블 조회
|
|
const tablesQuery = `
|
|
SELECT DISTINCT sd.table_name
|
|
FROM screen_menu_assignments sma
|
|
INNER JOIN screen_definitions sd ON sma.screen_id = sd.screen_id
|
|
WHERE sma.menu_objid = ANY($1)
|
|
AND sma.company_code = $2
|
|
AND sd.table_name IS NOT NULL
|
|
`;
|
|
|
|
const tablesResult = await pool.query(tablesQuery, [siblingObjids, companyCode]);
|
|
const tableNames = tablesResult.rows.map((row: any) => row.table_name);
|
|
|
|
logger.info("✅ 형제 메뉴 테이블 조회 완료", { tableNames, count: tableNames.length });
|
|
|
|
if (tableNames.length === 0) {
|
|
res.json({
|
|
success: true,
|
|
data: [],
|
|
message: "형제 메뉴에 연결된 테이블이 없습니다.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
const columnsQuery = `
|
|
SELECT
|
|
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.table_name = ANY($1)
|
|
AND ttc.company_code = $2
|
|
AND ttc.input_type = 'category'
|
|
ORDER BY ttc.table_name, ttc.column_name
|
|
`;
|
|
|
|
columnsResult = await pool.query(columnsQuery, [tableNames, companyCode]);
|
|
logger.info("✅ 레거시 방식 조회 완료", { 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();
|
|
}
|
|
}
|
|
|