Merge remote-tracking branch 'upstream/main'
All checks were successful
Build and Push Images / build-and-push (push) Successful in 10m13s

This commit is contained in:
kjs
2026-03-17 21:09:47 +09:00
64 changed files with 1445 additions and 559 deletions

View File

@@ -1,4 +1,5 @@
import "dotenv/config";
process.env.TZ = "Asia/Seoul";
import "express-async-errors"; // async 라우트 핸들러의 에러를 Express 에러 핸들러로 자동 전달
import express from "express";
import cors from "cors";

View File

@@ -10,7 +10,7 @@ export const getAuditLogs = async (
): Promise<void> => {
try {
const userCompanyCode = req.user?.companyCode;
const isSuperAdmin = userCompanyCode === "*";
const isSuperAdmin = req.user?.userType === "SUPER_ADMIN";
const {
companyCode,
@@ -63,7 +63,7 @@ export const getAuditLogStats = async (
): Promise<void> => {
try {
const userCompanyCode = req.user?.companyCode;
const isSuperAdmin = userCompanyCode === "*";
const isSuperAdmin = req.user?.userType === "SUPER_ADMIN";
const { companyCode, days } = req.query;
const targetCompany = isSuperAdmin
@@ -91,7 +91,7 @@ export const getAuditLogUsers = async (
): Promise<void> => {
try {
const userCompanyCode = req.user?.companyCode;
const isSuperAdmin = userCompanyCode === "*";
const isSuperAdmin = req.user?.userType === "SUPER_ADMIN";
const { companyCode } = req.query;
const conditions: string[] = ["LOWER(u.status) = 'active'"];

View File

@@ -224,6 +224,31 @@ export async function updateColumnSettings(
`컬럼 설정 업데이트 완료: ${tableName}.${columnName}, company: ${companyCode}`
);
auditLogService.log({
companyCode: companyCode || "",
userId: req.user?.userId || "",
userName: req.user?.userName || "",
action: "UPDATE",
resourceType: "TABLE",
resourceId: `${tableName}.${columnName}`,
resourceName: settings.columnLabel || columnName,
tableName: "table_type_columns",
summary: `테이블 타입관리: ${tableName}.${columnName} 컬럼 설정 변경`,
changes: {
after: {
columnLabel: settings.columnLabel,
inputType: settings.inputType,
referenceTable: settings.referenceTable,
referenceColumn: settings.referenceColumn,
displayColumn: settings.displayColumn,
codeCategory: settings.codeCategory,
},
fields: ["columnLabel", "inputType", "referenceTable", "referenceColumn", "displayColumn", "codeCategory"],
},
ipAddress: getClientIp(req),
requestPath: req.originalUrl,
});
const response: ApiResponse<null> = {
success: true,
message: "컬럼 설정을 성공적으로 저장했습니다.",
@@ -339,6 +364,29 @@ export async function updateAllColumnSettings(
`전체 컬럼 설정 일괄 업데이트 완료: ${tableName}, ${columnSettings.length}개, company: ${companyCode}`
);
const changedColumns = columnSettings
.filter((c) => c.columnName)
.map((c) => c.columnName)
.join(", ");
auditLogService.log({
companyCode: companyCode || "",
userId: req.user?.userId || "",
userName: req.user?.userName || "",
action: "BATCH_UPDATE",
resourceType: "TABLE",
resourceId: tableName,
resourceName: tableName,
tableName: "table_type_columns",
summary: `테이블 타입관리: ${tableName} 전체 컬럼 설정 일괄 변경 (${columnSettings.length}개)`,
changes: {
after: { columns: changedColumns, count: columnSettings.length },
fields: columnSettings.filter((c) => c.columnName).map((c) => c.columnName!),
},
ipAddress: getClientIp(req),
requestPath: req.originalUrl,
});
const response: ApiResponse<null> = {
success: true,
message: "모든 컬럼 설정을 성공적으로 저장했습니다.",

View File

@@ -66,8 +66,9 @@ export const initializePool = (): Pool => {
// 연결 풀 이벤트 핸들러
pool.on("connect", (client) => {
client.query("SET timezone = 'Asia/Seoul'");
if (config.debug) {
console.log("✅ PostgreSQL 클라이언트 연결 생성");
console.log("✅ PostgreSQL 클라이언트 연결 생성 (timezone: Asia/Seoul)");
}
});

View File

@@ -251,6 +251,28 @@ class AuditLogService {
[...params, limit, offset]
);
const SECURITY_MASK = "(보안 항목 - 값 비공개)";
const securedTables = ["table_type_columns"];
if (!isSuperAdmin) {
for (const entry of data) {
if (entry.table_name && securedTables.includes(entry.table_name) && entry.changes) {
const changes = typeof entry.changes === "string" ? JSON.parse(entry.changes) : entry.changes;
if (changes.before) {
for (const key of Object.keys(changes.before)) {
changes.before[key] = SECURITY_MASK;
}
}
if (changes.after) {
for (const key of Object.keys(changes.after)) {
changes.after[key] = SECURITY_MASK;
}
}
entry.changes = changes;
}
}
}
return { data, total };
}

View File

@@ -1707,71 +1707,66 @@ export class DynamicFormService {
try {
console.log(`🎯 제어관리 설정 확인 중... (screenId: ${screenId})`);
// 화면의 저장 버튼에서 제어관리 설정 조회
const screenLayouts = await query<{
component_id: string;
properties: any;
// V2 레이아웃에서 layout_data jsonb 조회
const v2Layouts = await query<{
layout_id: number;
layout_data: any;
}>(
`SELECT component_id, properties
FROM screen_layouts
WHERE screen_id = $1
AND component_type IN ('component', 'v2-button-primary')`,
[screenId]
`SELECT layout_id, layout_data
FROM screen_layouts_v2
WHERE screen_id = $1 AND company_code = $2`,
[screenId, companyCode]
);
console.log(`📋 화면 컴포넌트 조회 결과:`, screenLayouts.length);
if (v2Layouts.length === 0) {
console.log(` V2 레이아웃이 없습니다. (화면 ID: ${screenId}, company: ${companyCode})`);
return;
}
// layout_data.components 배열에서 버튼 컴포넌트 추출
const layoutData = v2Layouts[0].layout_data;
const components: any[] = layoutData?.components || [];
console.log(`📋 V2 컴포넌트 조회 결과: ${components.length}`);
// 저장 버튼 중에서 제어관리가 활성화된 것 찾기
let controlConfigFound = false;
for (const layout of screenLayouts) {
const properties = layout.properties as any;
for (const comp of components) {
const overrides = comp?.overrides || {};
// 디버깅: 모든 컴포넌트 정보 출력
console.log(`🔍 컴포넌트 검사:`, {
componentId: layout.component_id,
componentType: properties?.componentType,
actionType: properties?.componentConfig?.action?.type,
enableDataflowControl:
properties?.webTypeConfig?.enableDataflowControl,
hasDataflowConfig: !!properties?.webTypeConfig?.dataflowConfig,
hasDiagramId:
!!properties?.webTypeConfig?.dataflowConfig?.selectedDiagramId,
hasFlowControls:
!!properties?.webTypeConfig?.dataflowConfig?.flowControls,
});
const isButtonComponent =
overrides?.type === "v2-button-primary" ||
(comp?.url || "").includes("v2-button-primary");
// 버튼 컴포넌트이고 제어관리가 활성화된 경우
// triggerType에 맞는 액션 타입 매칭: insert/update -> save, delete -> delete
const buttonActionType = properties?.componentConfig?.action?.type;
const buttonActionType = overrides?.action?.type;
const isMatchingAction =
(triggerType === "delete" && buttonActionType === "delete") ||
((triggerType === "insert" || triggerType === "update") && buttonActionType === "save");
const isButtonComponent =
properties?.componentType === "button-primary" ||
properties?.componentType === "v2-button-primary";
console.log(`🔍 V2 컴포넌트 검사:`, {
componentId: comp?.id,
type: overrides?.type,
actionType: buttonActionType,
enableDataflowControl: overrides?.enableDataflowControl,
hasDataflowConfig: !!overrides?.dataflowConfig,
});
if (
isButtonComponent &&
isMatchingAction &&
properties?.webTypeConfig?.enableDataflowControl === true
overrides?.enableDataflowControl === true
) {
const dataflowConfig = properties?.webTypeConfig?.dataflowConfig;
// 다중 제어 설정 확인 (flowControls 배열)
const dataflowConfig = overrides?.dataflowConfig;
const flowControls = dataflowConfig?.flowControls || [];
// flowControls가 있으면 다중 제어 실행, 없으면 기존 단일 제어 실행
if (flowControls.length > 0) {
controlConfigFound = true;
console.log(`🎯 다중 제어관리 설정 발견: ${flowControls.length}`);
// 순서대로 정렬
const sortedControls = [...flowControls].sort(
(a: any, b: any) => (a.order || 0) - (b.order || 0)
);
// 다중 제어 순차 실행
await this.executeMultipleFlowControls(
sortedControls,
savedData,
@@ -1782,13 +1777,12 @@ export class DynamicFormService {
companyCode
);
} else if (dataflowConfig?.selectedDiagramId) {
// 기존 단일 제어 실행 (하위 호환성)
controlConfigFound = true;
const diagramId = dataflowConfig.selectedDiagramId;
const relationshipId = dataflowConfig.selectedRelationshipId;
console.log(`🎯 단일 제어관리 설정 발견:`, {
componentId: layout.component_id,
componentId: comp?.id,
diagramId,
relationshipId,
triggerType,
@@ -1806,7 +1800,6 @@ export class DynamicFormService {
);
}
// 첫 번째 설정된 버튼의 제어관리만 실행
break;
}
}