기본적인 crud 구현
This commit is contained in:
@@ -449,7 +449,13 @@ export async function getTableData(
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { tableName } = req.params;
|
||||
const { page = 1, size = 10, search = {}, sortBy, sortOrder = 'asc' } = req.body;
|
||||
const {
|
||||
page = 1,
|
||||
size = 10,
|
||||
search = {},
|
||||
sortBy,
|
||||
sortOrder = "asc",
|
||||
} = req.body;
|
||||
|
||||
logger.info(`=== 테이블 데이터 조회 시작: ${tableName} ===`);
|
||||
logger.info(`페이징: page=${page}, size=${size}`);
|
||||
@@ -470,20 +476,19 @@ export async function getTableData(
|
||||
}
|
||||
|
||||
const tableManagementService = new TableManagementService();
|
||||
|
||||
// 데이터 조회
|
||||
const result = await tableManagementService.getTableData(
|
||||
tableName,
|
||||
{
|
||||
page: parseInt(page),
|
||||
size: parseInt(size),
|
||||
search,
|
||||
sortBy,
|
||||
sortOrder
|
||||
}
|
||||
);
|
||||
|
||||
logger.info(`테이블 데이터 조회 완료: ${tableName}, 총 ${result.total}건, 페이지 ${result.page}/${result.totalPages}`);
|
||||
// 데이터 조회
|
||||
const result = await tableManagementService.getTableData(tableName, {
|
||||
page: parseInt(page),
|
||||
size: parseInt(size),
|
||||
search,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`테이블 데이터 조회 완료: ${tableName}, 총 ${result.total}건, 페이지 ${result.page}/${result.totalPages}`
|
||||
);
|
||||
|
||||
const response: ApiResponse<any> = {
|
||||
success: true,
|
||||
@@ -507,3 +512,234 @@ export async function getTableData(
|
||||
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();
|
||||
|
||||
// 데이터 추가
|
||||
await tableManagementService.addTableData(tableName, data);
|
||||
|
||||
logger.info(`테이블 데이터 추가 완료: ${tableName}`);
|
||||
|
||||
const response: ApiResponse<null> = {
|
||||
success: true,
|
||||
message: "테이블 데이터를 성공적으로 추가했습니다.",
|
||||
};
|
||||
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ import {
|
||||
getColumnLabels,
|
||||
updateColumnWebType,
|
||||
getTableData,
|
||||
addTableData,
|
||||
editTableData,
|
||||
deleteTableData,
|
||||
} from "../controllers/tableManagementController";
|
||||
|
||||
const router = express.Router();
|
||||
@@ -70,4 +73,22 @@ router.put(
|
||||
*/
|
||||
router.post("/tables/:tableName/data", getTableData);
|
||||
|
||||
/**
|
||||
* 테이블 데이터 추가
|
||||
* POST /api/table-management/tables/:tableName/add
|
||||
*/
|
||||
router.post("/tables/:tableName/add", addTableData);
|
||||
|
||||
/**
|
||||
* 테이블 데이터 수정
|
||||
* PUT /api/table-management/tables/:tableName/edit
|
||||
*/
|
||||
router.put("/tables/:tableName/edit", editTableData);
|
||||
|
||||
/**
|
||||
* 테이블 데이터 삭제
|
||||
* DELETE /api/table-management/tables/:tableName/delete
|
||||
*/
|
||||
router.delete("/tables/:tableName/delete", deleteTableData);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -519,7 +519,7 @@ export class TableManagementService {
|
||||
* 테이블 데이터 조회 (페이징 + 검색)
|
||||
*/
|
||||
async getTableData(
|
||||
tableName: string,
|
||||
tableName: string,
|
||||
options: {
|
||||
page: number;
|
||||
size: number;
|
||||
@@ -535,7 +535,7 @@ export class TableManagementService {
|
||||
totalPages: number;
|
||||
}> {
|
||||
try {
|
||||
const { page, size, search = {}, sortBy, sortOrder = 'asc' } = options;
|
||||
const { page, size, search = {}, sortBy, sortOrder = "asc" } = options;
|
||||
const offset = (page - 1) * size;
|
||||
|
||||
logger.info(`테이블 데이터 조회: ${tableName}`, options);
|
||||
@@ -547,11 +547,11 @@ export class TableManagementService {
|
||||
|
||||
if (search && Object.keys(search).length > 0) {
|
||||
for (const [column, value] of Object.entries(search)) {
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
if (value !== null && value !== undefined && value !== "") {
|
||||
// 안전한 컬럼명 검증 (SQL 인젝션 방지)
|
||||
const safeColumn = column.replace(/[^a-zA-Z0-9_]/g, '');
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const safeColumn = column.replace(/[^a-zA-Z0-9_]/g, "");
|
||||
|
||||
if (typeof value === "string") {
|
||||
whereConditions.push(`${safeColumn}::text ILIKE $${paramIndex}`);
|
||||
searchValues.push(`%${value}%`);
|
||||
} else {
|
||||
@@ -563,24 +563,29 @@ export class TableManagementService {
|
||||
}
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.length > 0
|
||||
? `WHERE ${whereConditions.join(' AND ')}`
|
||||
: '';
|
||||
const whereClause =
|
||||
whereConditions.length > 0
|
||||
? `WHERE ${whereConditions.join(" AND ")}`
|
||||
: "";
|
||||
|
||||
// ORDER BY 조건 구성
|
||||
let orderClause = '';
|
||||
let orderClause = "";
|
||||
if (sortBy) {
|
||||
const safeSortBy = sortBy.replace(/[^a-zA-Z0-9_]/g, '');
|
||||
const safeSortOrder = sortOrder.toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
||||
const safeSortBy = sortBy.replace(/[^a-zA-Z0-9_]/g, "");
|
||||
const safeSortOrder =
|
||||
sortOrder.toLowerCase() === "desc" ? "DESC" : "ASC";
|
||||
orderClause = `ORDER BY ${safeSortBy} ${safeSortOrder}`;
|
||||
}
|
||||
|
||||
// 안전한 테이블명 검증
|
||||
const safeTableName = tableName.replace(/[^a-zA-Z0-9_]/g, '');
|
||||
const safeTableName = tableName.replace(/[^a-zA-Z0-9_]/g, "");
|
||||
|
||||
// 전체 개수 조회
|
||||
const countQuery = `SELECT COUNT(*) as count FROM ${safeTableName} ${whereClause}`;
|
||||
const countResult = await prisma.$queryRawUnsafe<any[]>(countQuery, ...searchValues);
|
||||
const countResult = await prisma.$queryRawUnsafe<any[]>(
|
||||
countQuery,
|
||||
...searchValues
|
||||
);
|
||||
const total = parseInt(countResult[0].count);
|
||||
|
||||
// 데이터 조회
|
||||
@@ -590,29 +595,452 @@ export class TableManagementService {
|
||||
${orderClause}
|
||||
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
||||
`;
|
||||
|
||||
|
||||
const data = await prisma.$queryRawUnsafe<any[]>(
|
||||
dataQuery,
|
||||
...searchValues,
|
||||
size,
|
||||
dataQuery,
|
||||
...searchValues,
|
||||
size,
|
||||
offset
|
||||
);
|
||||
|
||||
const totalPages = Math.ceil(total / size);
|
||||
|
||||
logger.info(`테이블 데이터 조회 완료: ${tableName}, 총 ${total}건, ${data.length}개 반환`);
|
||||
logger.info(
|
||||
`테이블 데이터 조회 완료: ${tableName}, 총 ${total}건, ${data.length}개 반환`
|
||||
);
|
||||
|
||||
return {
|
||||
data,
|
||||
total,
|
||||
page,
|
||||
size,
|
||||
totalPages
|
||||
totalPages,
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`테이블 데이터 조회 오류: ${tableName}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 사용자 정보 조회 (JWT 토큰에서)
|
||||
*/
|
||||
private getCurrentUserFromRequest(req?: any): {
|
||||
userId: string;
|
||||
userName: string;
|
||||
} {
|
||||
// 실제 프로젝트에서는 req 객체에서 JWT 토큰을 파싱하여 사용자 정보를 가져올 수 있습니다
|
||||
// 현재는 기본값을 반환
|
||||
return {
|
||||
userId: "system",
|
||||
userName: "시스템 사용자",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 값을 PostgreSQL 타입에 맞게 변환
|
||||
*/
|
||||
private convertValueForPostgreSQL(value: any, dataType: string): any {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lowerDataType = dataType.toLowerCase();
|
||||
|
||||
// 날짜/시간 타입 처리
|
||||
if (
|
||||
lowerDataType.includes("timestamp") ||
|
||||
lowerDataType.includes("datetime")
|
||||
) {
|
||||
// YYYY-MM-DDTHH:mm:ss 형식을 PostgreSQL timestamp로 변환
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
const date = new Date(value);
|
||||
return date.toISOString();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// 날짜 타입 처리
|
||||
if (lowerDataType.includes("date")) {
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
// YYYY-MM-DD 형식 유지
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
||||
return value;
|
||||
}
|
||||
const date = new Date(value);
|
||||
return date.toISOString().split("T")[0];
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// 시간 타입 처리
|
||||
if (lowerDataType.includes("time")) {
|
||||
if (typeof value === "string") {
|
||||
// HH:mm:ss 형식 유지
|
||||
if (/^\d{2}:\d{2}:\d{2}$/.test(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// 숫자 타입 처리
|
||||
if (
|
||||
lowerDataType.includes("integer") ||
|
||||
lowerDataType.includes("bigint") ||
|
||||
lowerDataType.includes("serial")
|
||||
) {
|
||||
return parseInt(value) || null;
|
||||
}
|
||||
|
||||
if (
|
||||
lowerDataType.includes("numeric") ||
|
||||
lowerDataType.includes("decimal") ||
|
||||
lowerDataType.includes("real") ||
|
||||
lowerDataType.includes("double")
|
||||
) {
|
||||
return parseFloat(value) || null;
|
||||
}
|
||||
|
||||
// 불린 타입 처리
|
||||
if (lowerDataType.includes("boolean")) {
|
||||
if (typeof value === "string") {
|
||||
return value.toLowerCase() === "true" || value === "1";
|
||||
}
|
||||
return Boolean(value);
|
||||
}
|
||||
|
||||
// 기본적으로 문자열로 처리
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블에 데이터 추가
|
||||
*/
|
||||
async addTableData(
|
||||
tableName: string,
|
||||
data: Record<string, any>
|
||||
): Promise<void> {
|
||||
try {
|
||||
logger.info(`=== 테이블 데이터 추가 시작: ${tableName} ===`);
|
||||
logger.info(`추가할 데이터:`, data);
|
||||
|
||||
// 테이블의 컬럼 정보 조회
|
||||
const columnInfoQuery = `
|
||||
SELECT column_name, data_type, is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = $1
|
||||
ORDER BY ordinal_position
|
||||
`;
|
||||
|
||||
const columnInfoResult = (await prisma.$queryRawUnsafe(
|
||||
columnInfoQuery,
|
||||
tableName
|
||||
)) as any[];
|
||||
const columnTypeMap = new Map<string, string>();
|
||||
|
||||
columnInfoResult.forEach((col: any) => {
|
||||
columnTypeMap.set(col.column_name, col.data_type);
|
||||
});
|
||||
|
||||
logger.info(`컬럼 타입 정보:`, Object.fromEntries(columnTypeMap));
|
||||
|
||||
// 컬럼명과 값을 분리하고 타입에 맞게 변환
|
||||
const columns = Object.keys(data);
|
||||
const values = Object.values(data).map((value, index) => {
|
||||
const columnName = columns[index];
|
||||
const dataType = columnTypeMap.get(columnName) || "text";
|
||||
const convertedValue = this.convertValueForPostgreSQL(value, dataType);
|
||||
logger.info(
|
||||
`컬럼 "${columnName}" (${dataType}): "${value}" → "${convertedValue}"`
|
||||
);
|
||||
return convertedValue;
|
||||
});
|
||||
|
||||
// 동적 INSERT 쿼리 생성 (타입 캐스팅 포함)
|
||||
const placeholders = columns
|
||||
.map((col, index) => {
|
||||
const dataType = columnTypeMap.get(col) || "text";
|
||||
const lowerDataType = dataType.toLowerCase();
|
||||
|
||||
// PostgreSQL에서 직접 타입 캐스팅
|
||||
if (
|
||||
lowerDataType.includes("timestamp") ||
|
||||
lowerDataType.includes("datetime")
|
||||
) {
|
||||
return `$${index + 1}::timestamp`;
|
||||
} else if (lowerDataType.includes("date")) {
|
||||
return `$${index + 1}::date`;
|
||||
} else if (lowerDataType.includes("time")) {
|
||||
return `$${index + 1}::time`;
|
||||
} else if (
|
||||
lowerDataType.includes("integer") ||
|
||||
lowerDataType.includes("bigint") ||
|
||||
lowerDataType.includes("serial")
|
||||
) {
|
||||
return `$${index + 1}::integer`;
|
||||
} else if (
|
||||
lowerDataType.includes("numeric") ||
|
||||
lowerDataType.includes("decimal")
|
||||
) {
|
||||
return `$${index + 1}::numeric`;
|
||||
} else if (lowerDataType.includes("boolean")) {
|
||||
return `$${index + 1}::boolean`;
|
||||
}
|
||||
|
||||
return `$${index + 1}`;
|
||||
})
|
||||
.join(", ");
|
||||
const columnNames = columns.map((col) => `"${col}"`).join(", ");
|
||||
|
||||
const query = `
|
||||
INSERT INTO "${tableName}" (${columnNames})
|
||||
VALUES (${placeholders})
|
||||
`;
|
||||
|
||||
logger.info(`실행할 쿼리: ${query}`);
|
||||
logger.info(`쿼리 파라미터:`, values);
|
||||
|
||||
await prisma.$queryRawUnsafe(query, ...values);
|
||||
|
||||
logger.info(`테이블 데이터 추가 완료: ${tableName}`);
|
||||
} catch (error) {
|
||||
logger.error(`테이블 데이터 추가 오류: ${tableName}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 데이터 수정
|
||||
*/
|
||||
async editTableData(
|
||||
tableName: string,
|
||||
originalData: Record<string, any>,
|
||||
updatedData: Record<string, any>
|
||||
): Promise<void> {
|
||||
try {
|
||||
logger.info(`=== 테이블 데이터 수정 시작: ${tableName} ===`);
|
||||
logger.info(`원본 데이터:`, originalData);
|
||||
logger.info(`수정할 데이터:`, updatedData);
|
||||
|
||||
// 테이블의 컬럼 정보 조회 (PRIMARY KEY 찾기용)
|
||||
const columnInfoQuery = `
|
||||
SELECT c.column_name, c.data_type, c.is_nullable,
|
||||
CASE WHEN tc.constraint_type = 'PRIMARY KEY' THEN 'YES' ELSE 'NO' END as is_primary_key
|
||||
FROM information_schema.columns c
|
||||
LEFT JOIN information_schema.key_column_usage kcu ON c.column_name = kcu.column_name AND c.table_name = kcu.table_name
|
||||
LEFT JOIN information_schema.table_constraints tc ON kcu.constraint_name = tc.constraint_name AND tc.table_name = c.table_name
|
||||
WHERE c.table_name = $1
|
||||
ORDER BY c.ordinal_position
|
||||
`;
|
||||
|
||||
const columnInfoResult = (await prisma.$queryRawUnsafe(
|
||||
columnInfoQuery,
|
||||
tableName
|
||||
)) as any[];
|
||||
const columnTypeMap = new Map<string, string>();
|
||||
const primaryKeys: string[] = [];
|
||||
|
||||
columnInfoResult.forEach((col: any) => {
|
||||
columnTypeMap.set(col.column_name, col.data_type);
|
||||
if (col.is_primary_key === "YES") {
|
||||
primaryKeys.push(col.column_name);
|
||||
}
|
||||
});
|
||||
|
||||
logger.info(`컬럼 타입 정보:`, Object.fromEntries(columnTypeMap));
|
||||
logger.info(`PRIMARY KEY 컬럼들:`, primaryKeys);
|
||||
|
||||
// SET 절 생성 (수정할 데이터) - 먼저 생성
|
||||
const setConditions: string[] = [];
|
||||
const setValues: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
Object.keys(updatedData).forEach((column) => {
|
||||
const dataType = columnTypeMap.get(column) || "text";
|
||||
setConditions.push(
|
||||
`"${column}" = $${paramIndex}::${this.getPostgreSQLType(dataType)}`
|
||||
);
|
||||
setValues.push(
|
||||
this.convertValueForPostgreSQL(updatedData[column], dataType)
|
||||
);
|
||||
paramIndex++;
|
||||
});
|
||||
|
||||
// WHERE 조건 생성 (PRIMARY KEY 우선, 없으면 모든 원본 데이터 사용)
|
||||
let whereConditions: string[] = [];
|
||||
let whereValues: any[] = [];
|
||||
|
||||
if (primaryKeys.length > 0) {
|
||||
// PRIMARY KEY로 WHERE 조건 생성
|
||||
primaryKeys.forEach((pkColumn) => {
|
||||
if (originalData[pkColumn] !== undefined) {
|
||||
const dataType = columnTypeMap.get(pkColumn) || "text";
|
||||
whereConditions.push(
|
||||
`"${pkColumn}" = $${paramIndex}::${this.getPostgreSQLType(dataType)}`
|
||||
);
|
||||
whereValues.push(
|
||||
this.convertValueForPostgreSQL(originalData[pkColumn], dataType)
|
||||
);
|
||||
paramIndex++;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// PRIMARY KEY가 없으면 모든 원본 데이터로 WHERE 조건 생성
|
||||
Object.keys(originalData).forEach((column) => {
|
||||
const dataType = columnTypeMap.get(column) || "text";
|
||||
whereConditions.push(
|
||||
`"${column}" = $${paramIndex}::${this.getPostgreSQLType(dataType)}`
|
||||
);
|
||||
whereValues.push(
|
||||
this.convertValueForPostgreSQL(originalData[column], dataType)
|
||||
);
|
||||
paramIndex++;
|
||||
});
|
||||
}
|
||||
|
||||
// UPDATE 쿼리 생성
|
||||
const query = `
|
||||
UPDATE "${tableName}"
|
||||
SET ${setConditions.join(", ")}
|
||||
WHERE ${whereConditions.join(" AND ")}
|
||||
`;
|
||||
|
||||
const allValues = [...setValues, ...whereValues];
|
||||
|
||||
logger.info(`실행할 UPDATE 쿼리: ${query}`);
|
||||
logger.info(`쿼리 파라미터:`, allValues);
|
||||
|
||||
const result = await prisma.$queryRawUnsafe(query, ...allValues);
|
||||
|
||||
logger.info(`테이블 데이터 수정 완료: ${tableName}`, result);
|
||||
} catch (error) {
|
||||
logger.error(`테이블 데이터 수정 오류: ${tableName}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PostgreSQL 타입명 반환
|
||||
*/
|
||||
private getPostgreSQLType(dataType: string): string {
|
||||
const lowerDataType = dataType.toLowerCase();
|
||||
|
||||
if (
|
||||
lowerDataType.includes("timestamp") ||
|
||||
lowerDataType.includes("datetime")
|
||||
) {
|
||||
return "timestamp";
|
||||
} else if (lowerDataType.includes("date")) {
|
||||
return "date";
|
||||
} else if (lowerDataType.includes("time")) {
|
||||
return "time";
|
||||
} else if (
|
||||
lowerDataType.includes("integer") ||
|
||||
lowerDataType.includes("bigint") ||
|
||||
lowerDataType.includes("serial")
|
||||
) {
|
||||
return "integer";
|
||||
} else if (
|
||||
lowerDataType.includes("numeric") ||
|
||||
lowerDataType.includes("decimal")
|
||||
) {
|
||||
return "numeric";
|
||||
} else if (lowerDataType.includes("boolean")) {
|
||||
return "boolean";
|
||||
}
|
||||
|
||||
return "text"; // 기본값
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블에서 데이터 삭제
|
||||
*/
|
||||
async deleteTableData(
|
||||
tableName: string,
|
||||
dataToDelete: Record<string, any>[]
|
||||
): Promise<number> {
|
||||
try {
|
||||
logger.info(`테이블 데이터 삭제: ${tableName}`, dataToDelete);
|
||||
|
||||
if (!Array.isArray(dataToDelete) || dataToDelete.length === 0) {
|
||||
throw new Error("삭제할 데이터가 없습니다.");
|
||||
}
|
||||
|
||||
let deletedCount = 0;
|
||||
|
||||
// 테이블의 기본 키 컬럼 찾기 (정확한 식별을 위해)
|
||||
const primaryKeyQuery = `
|
||||
SELECT column_name
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
WHERE tc.table_name = $1
|
||||
AND tc.constraint_type = 'PRIMARY KEY'
|
||||
ORDER BY kcu.ordinal_position
|
||||
`;
|
||||
|
||||
const primaryKeys = await prisma.$queryRawUnsafe<
|
||||
{ column_name: string }[]
|
||||
>(primaryKeyQuery, tableName);
|
||||
|
||||
if (primaryKeys.length === 0) {
|
||||
// 기본 키가 없는 경우, 모든 컬럼으로 삭제 조건 생성
|
||||
logger.warn(
|
||||
`테이블 ${tableName}에 기본 키가 없습니다. 모든 컬럼으로 삭제 조건을 생성합니다.`
|
||||
);
|
||||
|
||||
for (const rowData of dataToDelete) {
|
||||
const conditions = Object.keys(rowData)
|
||||
.map((key, index) => `"${key}" = $${index + 1}`)
|
||||
.join(" AND ");
|
||||
|
||||
const values = Object.values(rowData);
|
||||
|
||||
const deleteQuery = `DELETE FROM "${tableName}" WHERE ${conditions}`;
|
||||
|
||||
const result = await prisma.$queryRawUnsafe(deleteQuery, ...values);
|
||||
deletedCount += Number(result);
|
||||
}
|
||||
} else {
|
||||
// 기본 키를 사용한 삭제
|
||||
const primaryKeyNames = primaryKeys.map((pk) => pk.column_name);
|
||||
|
||||
for (const rowData of dataToDelete) {
|
||||
const conditions = primaryKeyNames
|
||||
.map((key, index) => `"${key}" = $${index + 1}`)
|
||||
.join(" AND ");
|
||||
|
||||
const values = primaryKeyNames.map((key) => rowData[key]);
|
||||
|
||||
// null 값이 있는 경우 스킵
|
||||
if (values.some((val) => val === null || val === undefined)) {
|
||||
logger.warn(`기본 키 값이 null인 행을 스킵합니다:`, rowData);
|
||||
continue;
|
||||
}
|
||||
|
||||
const deleteQuery = `DELETE FROM "${tableName}" WHERE ${conditions}`;
|
||||
|
||||
const result = await prisma.$queryRawUnsafe(deleteQuery, ...values);
|
||||
deletedCount += Number(result);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`테이블 데이터 삭제 완료: ${tableName}, ${deletedCount}건 삭제`
|
||||
);
|
||||
return deletedCount;
|
||||
} catch (error) {
|
||||
logger.error(`테이블 데이터 삭제 오류: ${tableName}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user