행 이동 화면 할당한 상황에서도 가능하게, 코드병합 버튼액션에 추가
This commit is contained in:
282
backend-node/src/controllers/codeMergeController.ts
Normal file
282
backend-node/src/controllers/codeMergeController.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
import { Request, Response } from "express";
|
||||
import pool from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
interface AuthenticatedRequest extends Request {
|
||||
user?: {
|
||||
userId: string;
|
||||
userName: string;
|
||||
companyCode: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드 병합 - 모든 관련 테이블에 적용
|
||||
* 데이터(레코드)는 삭제하지 않고, 컬럼 값만 변경
|
||||
*/
|
||||
export async function mergeCodeAllTables(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
const { columnName, oldValue, newValue } = req.body;
|
||||
const companyCode = req.user?.companyCode;
|
||||
|
||||
try {
|
||||
// 입력값 검증
|
||||
if (!columnName || !oldValue || !newValue) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "필수 필드가 누락되었습니다. (columnName, oldValue, newValue)",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!companyCode) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
message: "인증 정보가 없습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 같은 값으로 병합 시도 방지
|
||||
if (oldValue === newValue) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "기존 값과 새 값이 동일합니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("코드 병합 시작", {
|
||||
columnName,
|
||||
oldValue,
|
||||
newValue,
|
||||
companyCode,
|
||||
userId: req.user?.userId,
|
||||
});
|
||||
|
||||
// PostgreSQL 함수 호출
|
||||
const result = await pool.query(
|
||||
"SELECT * FROM merge_code_all_tables($1, $2, $3, $4)",
|
||||
[columnName, oldValue, newValue, companyCode]
|
||||
);
|
||||
|
||||
// 결과 처리 (pool.query 반환 타입 처리)
|
||||
const affectedTables = Array.isArray(result) ? result : (result.rows || []);
|
||||
const totalRows = affectedTables.reduce(
|
||||
(sum, row) => sum + parseInt(row.rows_updated || 0),
|
||||
0
|
||||
);
|
||||
|
||||
logger.info("코드 병합 완료", {
|
||||
columnName,
|
||||
oldValue,
|
||||
newValue,
|
||||
affectedTablesCount: affectedTables.length,
|
||||
totalRowsUpdated: totalRows,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `코드 병합 완료: ${oldValue} → ${newValue}`,
|
||||
data: {
|
||||
columnName,
|
||||
oldValue,
|
||||
newValue,
|
||||
affectedTables: affectedTables.map((row) => ({
|
||||
tableName: row.table_name,
|
||||
rowsUpdated: parseInt(row.rows_updated),
|
||||
})),
|
||||
totalRowsUpdated: totalRows,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("코드 병합 실패:", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
columnName,
|
||||
oldValue,
|
||||
newValue,
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "코드 병합 중 오류가 발생했습니다.",
|
||||
error: {
|
||||
code: "CODE_MERGE_ERROR",
|
||||
details: error.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 컬럼을 가진 테이블 목록 조회
|
||||
*/
|
||||
export async function getTablesWithColumn(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
const { columnName } = req.params;
|
||||
|
||||
try {
|
||||
if (!columnName) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "컬럼명이 필요합니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("컬럼을 가진 테이블 목록 조회", { columnName });
|
||||
|
||||
const query = `
|
||||
SELECT DISTINCT t.table_name
|
||||
FROM information_schema.columns c
|
||||
JOIN information_schema.tables t
|
||||
ON c.table_name = t.table_name
|
||||
WHERE c.column_name = $1
|
||||
AND t.table_schema = 'public'
|
||||
AND t.table_type = 'BASE TABLE'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM information_schema.columns c2
|
||||
WHERE c2.table_name = t.table_name
|
||||
AND c2.column_name = 'company_code'
|
||||
)
|
||||
ORDER BY t.table_name
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [columnName]);
|
||||
|
||||
logger.info(`컬럼을 가진 테이블 조회 완료: ${result.rows.length}개`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "테이블 목록 조회 성공",
|
||||
data: {
|
||||
columnName,
|
||||
tables: result.rows.map((row) => row.table_name),
|
||||
count: result.rows.length,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("테이블 목록 조회 실패:", error);
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "테이블 목록 조회 중 오류가 발생했습니다.",
|
||||
error: {
|
||||
code: "TABLE_LIST_ERROR",
|
||||
details: error.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드 병합 미리보기 (실제 실행 없이 영향받을 데이터 확인)
|
||||
*/
|
||||
export async function previewCodeMerge(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
const { columnName, oldValue } = req.body;
|
||||
const companyCode = req.user?.companyCode;
|
||||
|
||||
try {
|
||||
if (!columnName || !oldValue) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "필수 필드가 누락되었습니다. (columnName, oldValue)",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!companyCode) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
message: "인증 정보가 없습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("코드 병합 미리보기", { columnName, oldValue, companyCode });
|
||||
|
||||
// 해당 컬럼을 가진 테이블 찾기
|
||||
const tablesQuery = `
|
||||
SELECT DISTINCT t.table_name
|
||||
FROM information_schema.columns c
|
||||
JOIN information_schema.tables t
|
||||
ON c.table_name = t.table_name
|
||||
WHERE c.column_name = $1
|
||||
AND t.table_schema = 'public'
|
||||
AND t.table_type = 'BASE TABLE'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM information_schema.columns c2
|
||||
WHERE c2.table_name = t.table_name
|
||||
AND c2.column_name = 'company_code'
|
||||
)
|
||||
`;
|
||||
|
||||
const tablesResult = await pool.query(tablesQuery, [columnName]);
|
||||
|
||||
// 각 테이블에서 영향받을 행 수 계산
|
||||
const preview = [];
|
||||
const tableRows = Array.isArray(tablesResult) ? tablesResult : (tablesResult.rows || []);
|
||||
|
||||
for (const row of tableRows) {
|
||||
const tableName = row.table_name;
|
||||
|
||||
// 동적 SQL 생성 (테이블명과 컬럼명은 파라미터 바인딩 불가)
|
||||
// SQL 인젝션 방지: 테이블명과 컬럼명은 information_schema에서 검증된 값
|
||||
const countQuery = `SELECT COUNT(*) as count FROM "${tableName}" WHERE "${columnName}" = $1 AND company_code = $2`;
|
||||
|
||||
try {
|
||||
const countResult = await pool.query(countQuery, [oldValue, companyCode]);
|
||||
const count = parseInt(countResult.rows[0].count);
|
||||
|
||||
if (count > 0) {
|
||||
preview.push({
|
||||
tableName,
|
||||
affectedRows: count,
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.warn(`테이블 ${tableName} 조회 실패:`, error.message);
|
||||
// 테이블 접근 실패 시 건너뛰기
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const totalRows = preview.reduce((sum, item) => sum + item.affectedRows, 0);
|
||||
|
||||
logger.info("코드 병합 미리보기 완료", {
|
||||
tablesCount: preview.length,
|
||||
totalRows,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "코드 병합 미리보기 완료",
|
||||
data: {
|
||||
columnName,
|
||||
oldValue,
|
||||
preview,
|
||||
totalAffectedRows: totalRows,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("코드 병합 미리보기 실패:", error);
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "코드 병합 미리보기 중 오류가 발생했습니다.",
|
||||
error: {
|
||||
code: "PREVIEW_ERROR",
|
||||
details: error.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user