2025-11-04 18:31:26 +09:00
|
|
|
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 반환 타입 처리)
|
2025-11-07 10:18:34 +09:00
|
|
|
const affectedTables = Array.isArray(result) ? result : ((result as any).rows || []);
|
2025-11-04 18:31:26 +09:00
|
|
|
const totalRows = affectedTables.reduce(
|
2025-11-07 10:18:34 +09:00
|
|
|
(sum: number, row: any) => sum + parseInt(row.rows_updated || 0),
|
2025-11-04 18:31:26 +09:00
|
|
|
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]);
|
2025-11-07 10:18:34 +09:00
|
|
|
const rows = (result as any).rows || [];
|
2025-11-04 18:31:26 +09:00
|
|
|
|
2025-11-07 10:18:34 +09:00
|
|
|
logger.info(`컬럼을 가진 테이블 조회 완료: ${rows.length}개`);
|
2025-11-04 18:31:26 +09:00
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
message: "테이블 목록 조회 성공",
|
|
|
|
|
data: {
|
|
|
|
|
columnName,
|
2025-11-07 10:18:34 +09:00
|
|
|
tables: rows.map((row: any) => row.table_name),
|
|
|
|
|
count: rows.length,
|
2025-11-04 18:31:26 +09:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
} 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 = [];
|
2025-11-07 10:18:34 +09:00
|
|
|
const tableRows = Array.isArray(tablesResult) ? tablesResult : ((tablesResult as any).rows || []);
|
2025-11-04 18:31:26 +09:00
|
|
|
|
|
|
|
|
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]);
|
2025-11-07 10:18:34 +09:00
|
|
|
const rows = (countResult as any).rows || [];
|
|
|
|
|
const count = rows.length > 0 ? parseInt(rows[0].count) : 0;
|
2025-11-04 18:31:26 +09:00
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|