플로우 로그기능 수정
This commit is contained in:
parent
93675100da
commit
74ebb565e6
|
|
@ -31,13 +31,16 @@ export class FlowController {
|
|||
*/
|
||||
createFlowDefinition = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { name, description, tableName } = req.body;
|
||||
const { name, description, tableName, dbSourceType, dbConnectionId } =
|
||||
req.body;
|
||||
const userId = (req as any).user?.userId || "system";
|
||||
|
||||
console.log("🔍 createFlowDefinition called with:", {
|
||||
name,
|
||||
description,
|
||||
tableName,
|
||||
dbSourceType,
|
||||
dbConnectionId,
|
||||
});
|
||||
|
||||
if (!name) {
|
||||
|
|
@ -62,7 +65,7 @@ export class FlowController {
|
|||
}
|
||||
|
||||
const flowDef = await this.flowDefinitionService.create(
|
||||
{ name, description, tableName },
|
||||
{ name, description, tableName, dbSourceType, dbConnectionId },
|
||||
userId
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import { Router } from "express";
|
||||
import { FlowController } from "../controllers/flowController";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
|
||||
const router = Router();
|
||||
const flowController = new FlowController();
|
||||
|
|
@ -32,8 +33,8 @@ router.get("/:flowId/step/:stepId/list", flowController.getStepDataList);
|
|||
router.get("/:flowId/steps/counts", flowController.getAllStepCounts);
|
||||
|
||||
// ==================== 데이터 이동 ====================
|
||||
router.post("/move", flowController.moveData);
|
||||
router.post("/move-batch", flowController.moveBatchData);
|
||||
router.post("/move", authenticateToken, flowController.moveData);
|
||||
router.post("/move-batch", authenticateToken, flowController.moveBatchData);
|
||||
|
||||
// ==================== 오딧 로그 ====================
|
||||
router.get("/audit/:flowId/:recordId", flowController.getAuditLogs);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
import { Pool as PgPool } from "pg";
|
||||
import * as mysql from "mysql2/promise";
|
||||
import db from "../database/db";
|
||||
import { CredentialEncryption } from "../utils/credentialEncryption";
|
||||
import { PasswordEncryption } from "../utils/passwordEncryption";
|
||||
import {
|
||||
getConnectionTestQuery,
|
||||
getPlaceholder,
|
||||
|
|
@ -31,24 +31,13 @@ interface ExternalDbConnection {
|
|||
// 외부 DB 연결 풀 캐시 (타입별로 다른 풀 객체)
|
||||
const connectionPools = new Map<number, any>();
|
||||
|
||||
// 비밀번호 복호화 유틸
|
||||
const credentialEncryption = new CredentialEncryption(
|
||||
process.env.ENCRYPTION_SECRET_KEY || "default-secret-key-change-in-production"
|
||||
);
|
||||
|
||||
/**
|
||||
* 외부 DB 연결 정보 조회
|
||||
*/
|
||||
async function getExternalConnection(
|
||||
connectionId: number
|
||||
): Promise<ExternalDbConnection | null> {
|
||||
const query = `
|
||||
SELECT
|
||||
id, connection_name, db_type, host, port,
|
||||
database_name, username, encrypted_password, is_active
|
||||
FROM external_db_connections
|
||||
WHERE id = $1 AND is_active = true
|
||||
`;
|
||||
const query = `SELECT * FROM external_db_connections WHERE id = $1 AND is_active = 'Y'`;
|
||||
|
||||
const result = await db.query(query, [connectionId]);
|
||||
|
||||
|
|
@ -58,13 +47,14 @@ async function getExternalConnection(
|
|||
|
||||
const row = result[0];
|
||||
|
||||
// 비밀번호 복호화
|
||||
// 비밀번호 복호화 (암호화된 비밀번호는 password 컬럼에 저장됨)
|
||||
let decryptedPassword = "";
|
||||
try {
|
||||
decryptedPassword = credentialEncryption.decrypt(row.encrypted_password);
|
||||
decryptedPassword = PasswordEncryption.decrypt(row.password);
|
||||
} catch (error) {
|
||||
console.error(`비밀번호 복호화 실패 (ID: ${connectionId}):`, error);
|
||||
throw new Error("외부 DB 비밀번호 복호화에 실패했습니다");
|
||||
// 복호화 실패 시 원본 비밀번호 사용 (fallback)
|
||||
decryptedPassword = row.password;
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -161,6 +161,28 @@ export class FlowDataMoveService {
|
|||
}
|
||||
|
||||
// 5. 감사 로그 기록
|
||||
let dbConnectionName = null;
|
||||
if (
|
||||
flowDefinition.dbSourceType === "external" &&
|
||||
flowDefinition.dbConnectionId
|
||||
) {
|
||||
// 외부 DB인 경우 연결 이름 조회
|
||||
try {
|
||||
const connResult = await client.query(
|
||||
`SELECT connection_name FROM external_db_connections WHERE id = $1`,
|
||||
[flowDefinition.dbConnectionId]
|
||||
);
|
||||
if (connResult.rows && connResult.rows.length > 0) {
|
||||
dbConnectionName = connResult.rows[0].connection_name;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("외부 DB 연결 이름 조회 실패:", error);
|
||||
}
|
||||
} else {
|
||||
// 내부 DB인 경우
|
||||
dbConnectionName = "내부 데이터베이스";
|
||||
}
|
||||
|
||||
await this.logDataMove(client, {
|
||||
flowId,
|
||||
fromStepId,
|
||||
|
|
@ -173,6 +195,11 @@ export class FlowDataMoveService {
|
|||
statusFrom: fromStep.statusValue,
|
||||
statusTo: toStep.statusValue,
|
||||
userId,
|
||||
dbConnectionId:
|
||||
flowDefinition.dbSourceType === "external"
|
||||
? flowDefinition.dbConnectionId
|
||||
: null,
|
||||
dbConnectionName,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
@ -361,8 +388,9 @@ export class FlowDataMoveService {
|
|||
move_type, source_table, target_table,
|
||||
source_data_id, target_data_id,
|
||||
status_from, status_to,
|
||||
changed_by, note
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
changed_by, note,
|
||||
db_connection_id, db_connection_name
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||
`;
|
||||
|
||||
await client.query(query, [
|
||||
|
|
@ -378,6 +406,8 @@ export class FlowDataMoveService {
|
|||
params.statusTo,
|
||||
params.userId,
|
||||
params.note || null,
|
||||
params.dbConnectionId || null,
|
||||
params.dbConnectionName || null,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -452,6 +482,8 @@ export class FlowDataMoveService {
|
|||
targetDataId: row.target_data_id,
|
||||
statusFrom: row.status_from,
|
||||
statusTo: row.status_to,
|
||||
dbConnectionId: row.db_connection_id,
|
||||
dbConnectionName: row.db_connection_name,
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
@ -496,6 +528,8 @@ export class FlowDataMoveService {
|
|||
targetDataId: row.target_data_id,
|
||||
statusFrom: row.status_from,
|
||||
statusTo: row.status_to,
|
||||
dbConnectionId: row.db_connection_id,
|
||||
dbConnectionName: row.db_connection_name,
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
@ -718,7 +752,21 @@ export class FlowDataMoveService {
|
|||
|
||||
// 3. 외부 연동 처리는 생략 (외부 DB 자체가 외부이므로)
|
||||
|
||||
// 4. 감사 로그 기록 (내부 DB에)
|
||||
// 4. 외부 DB 연결 이름 조회
|
||||
let dbConnectionName = null;
|
||||
try {
|
||||
const connResult = await db.query(
|
||||
`SELECT connection_name FROM external_db_connections WHERE id = $1`,
|
||||
[dbConnectionId]
|
||||
);
|
||||
if (connResult.length > 0) {
|
||||
dbConnectionName = connResult[0].connection_name;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("외부 DB 연결 이름 조회 실패:", error);
|
||||
}
|
||||
|
||||
// 5. 감사 로그 기록 (내부 DB에)
|
||||
// 외부 DB는 내부 DB 트랜잭션 외부이므로 직접 쿼리 실행
|
||||
const auditQuery = `
|
||||
INSERT INTO flow_audit_log (
|
||||
|
|
@ -726,8 +774,9 @@ export class FlowDataMoveService {
|
|||
move_type, source_table, target_table,
|
||||
source_data_id, target_data_id,
|
||||
status_from, status_to,
|
||||
changed_by, note
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
changed_by, note,
|
||||
db_connection_id, db_connection_name
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||
`;
|
||||
|
||||
await db.query(auditQuery, [
|
||||
|
|
@ -743,6 +792,8 @@ export class FlowDataMoveService {
|
|||
toStep.statusValue || null, // statusTo
|
||||
userId,
|
||||
`외부 DB (${dbType}) 데이터 이동`,
|
||||
dbConnectionId,
|
||||
dbConnectionName,
|
||||
]);
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -182,6 +182,9 @@ export interface FlowAuditLog {
|
|||
targetDataId?: string;
|
||||
statusFrom?: string;
|
||||
statusTo?: string;
|
||||
// 외부 DB 연결 정보
|
||||
dbConnectionId?: number;
|
||||
dbConnectionName?: string;
|
||||
// 조인 필드
|
||||
fromStepName?: string;
|
||||
toStepName?: string;
|
||||
|
|
|
|||
|
|
@ -243,14 +243,20 @@ export function FlowConditionBuilder({
|
|||
<SelectValue placeholder="컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{columns.map((col) => (
|
||||
<SelectItem key={col.columnName} value={col.columnName}>
|
||||
{columns.map((col, idx) => {
|
||||
const columnName = col.column_name || col.columnName || "";
|
||||
const dataType = col.data_type || col.dataType || "";
|
||||
const displayName = col.displayName || col.display_name || columnName;
|
||||
|
||||
return (
|
||||
<SelectItem key={`${columnName}-${idx}`} value={columnName}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{col.displayName || col.columnName}</span>
|
||||
<span className="text-xs text-gray-500">({col.dataType})</span>
|
||||
<span className="font-medium">{displayName}</span>
|
||||
<span className="text-xs text-gray-500">({dataType})</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -666,8 +666,12 @@ export function FlowStepPanel({
|
|||
{loadingColumns
|
||||
? "컬럼 로딩 중..."
|
||||
: formData.statusColumn
|
||||
? columns.find((col) => col.columnName === formData.statusColumn)?.columnName ||
|
||||
formData.statusColumn
|
||||
? (() => {
|
||||
const col = columns.find(
|
||||
(c) => (c.column_name || c.columnName) === formData.statusColumn,
|
||||
);
|
||||
return col ? col.column_name || col.columnName : formData.statusColumn;
|
||||
})()
|
||||
: "상태 컬럼 선택"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
|
|
@ -678,27 +682,32 @@ export function FlowStepPanel({
|
|||
<CommandList>
|
||||
<CommandEmpty>컬럼을 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{columns.map((column) => (
|
||||
{columns.map((column, idx) => {
|
||||
const columnName = column.column_name || column.columnName || "";
|
||||
const dataType = column.data_type || column.dataType || "";
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
key={column.columnName}
|
||||
value={column.columnName}
|
||||
key={`${columnName}-${idx}`}
|
||||
value={columnName}
|
||||
onSelect={() => {
|
||||
setFormData({ ...formData, statusColumn: column.columnName });
|
||||
setFormData({ ...formData, statusColumn: columnName });
|
||||
setOpenStatusColumnCombobox(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
formData.statusColumn === column.columnName ? "opacity-100" : "opacity-0",
|
||||
formData.statusColumn === columnName ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<div>{column.columnName}</div>
|
||||
<div className="text-xs text-gray-500">({column.dataType})</div>
|
||||
<div>{columnName}</div>
|
||||
<div className="text-xs text-gray-500">({dataType})</div>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
|
|
|
|||
|
|
@ -397,6 +397,7 @@ export function FlowWidget({ component, onStepClick }: FlowWidgetProps) {
|
|||
<TableHead className="w-[100px]">데이터 ID</TableHead>
|
||||
<TableHead className="w-[140px]">상태 변경</TableHead>
|
||||
<TableHead className="w-[100px]">변경자</TableHead>
|
||||
<TableHead className="w-[150px]">DB 연결</TableHead>
|
||||
<TableHead>테이블</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
|
@ -450,6 +451,21 @@ export function FlowWidget({ component, onStepClick }: FlowWidgetProps) {
|
|||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">{log.changedBy}</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{log.dbConnectionName ? (
|
||||
<span
|
||||
className={
|
||||
log.dbConnectionName === "내부 데이터베이스"
|
||||
? "text-blue-600"
|
||||
: "text-green-600"
|
||||
}
|
||||
>
|
||||
{log.dbConnectionName}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{log.sourceTable || "-"}
|
||||
{log.targetTable && log.targetTable !== log.sourceTable && (
|
||||
|
|
|
|||
|
|
@ -21,6 +21,12 @@ import {
|
|||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL || "/api";
|
||||
|
||||
// 토큰 가져오기
|
||||
function getAuthToken(): string | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
return localStorage.getItem("authToken") || sessionStorage.getItem("authToken");
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 플로우 정의 API
|
||||
// ============================================
|
||||
|
|
@ -364,10 +370,12 @@ export async function getAllStepCounts(flowId: number): Promise<ApiResponse<Flow
|
|||
*/
|
||||
export async function moveData(data: MoveDataRequest): Promise<ApiResponse<{ success: boolean }>> {
|
||||
try {
|
||||
const token = getAuthToken();
|
||||
const response = await fetch(`${API_BASE}/flow/move`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(token && { Authorization: `Bearer ${token}` }),
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(data),
|
||||
|
|
@ -404,10 +412,12 @@ export async function moveBatchData(
|
|||
data: MoveBatchDataRequest,
|
||||
): Promise<ApiResponse<{ success: boolean; results: any[] }>> {
|
||||
try {
|
||||
const token = getAuthToken();
|
||||
const response = await fetch(`${API_BASE}/flow/move-batch`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(token && { Authorization: `Bearer ${token}` }),
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(data),
|
||||
|
|
|
|||
|
|
@ -148,6 +148,15 @@ export interface FlowAuditLog {
|
|||
note?: string;
|
||||
fromStepName?: string;
|
||||
toStepName?: string;
|
||||
moveType?: "status" | "table" | "both";
|
||||
sourceTable?: string;
|
||||
targetTable?: string;
|
||||
sourceDataId?: string;
|
||||
targetDataId?: string;
|
||||
statusFrom?: string;
|
||||
statusTo?: string;
|
||||
dbConnectionId?: number;
|
||||
dbConnectionName?: string;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
|
|
|
|||
Loading…
Reference in New Issue