Merge remote-tracking branch 'upstream/main'
Some checks failed
Build and Push Images / build-and-push (push) Failing after 1m3s

This commit is contained in:
kjs
2026-02-23 12:18:49 +09:00
22 changed files with 1561 additions and 1258 deletions

View File

@@ -18,45 +18,6 @@ import { pool } from "../database/db"; // 🆕 Entity 조인을 위한 pool impo
import { buildDataFilterWhereClause } from "../utils/dataFilterUtil"; // 🆕 데이터 필터 유틸
import { v4 as uuidv4 } from "uuid"; // 🆕 UUID 생성
/**
* 비밀번호(password) 타입 컬럼의 값을 빈 문자열로 마스킹
* - table_type_columns에서 input_type = 'password'인 컬럼을 조회
* - 데이터 응답에서 해당 컬럼 값을 비워서 해시값 노출 방지
*/
async function maskPasswordColumns(tableName: string, data: any): Promise<any> {
try {
const passwordCols = await query<{ column_name: string }>(
`SELECT DISTINCT column_name FROM table_type_columns
WHERE table_name = $1 AND input_type = 'password'`,
[tableName]
);
if (passwordCols.length === 0) return data;
const passwordColumnNames = new Set(passwordCols.map(c => c.column_name));
// 단일 객체 처리
const maskRow = (row: any) => {
if (!row || typeof row !== "object") return row;
const masked = { ...row };
for (const col of passwordColumnNames) {
if (col in masked) {
masked[col] = ""; // 해시값 대신 빈 문자열
}
}
return masked;
};
if (Array.isArray(data)) {
return data.map(maskRow);
}
return maskRow(data);
} catch (error) {
// 마스킹 실패해도 원본 데이터 반환 (서비스 중단 방지)
console.warn("⚠️ password 컬럼 마스킹 실패:", error);
return data;
}
}
interface GetTableDataParams {
tableName: string;
limit?: number;
@@ -661,14 +622,14 @@ class DataService {
return {
success: true,
data: await maskPasswordColumns(tableName, normalizedGroupRows), // 🔧 배열로 반환! + password 마스킹
data: normalizedGroupRows, // 🔧 배열로 반환!
};
}
}
return {
success: true,
data: await maskPasswordColumns(tableName, normalizedRows[0]), // 그룹핑 없으면 단일 레코드 + password 마스킹
data: normalizedRows[0], // 그룹핑 없으면 단일 레코드
};
}
}
@@ -687,7 +648,7 @@ class DataService {
return {
success: true,
data: await maskPasswordColumns(tableName, result[0]), // password 마스킹
data: result[0],
};
} catch (error) {
console.error(`레코드 상세 조회 오류 (${tableName}/${id}):`, error);

View File

@@ -2,7 +2,6 @@ import { query, queryOne, transaction, getPool } from "../database/db";
import { EventTriggerService } from "./eventTriggerService";
import { DataflowControlService } from "./dataflowControlService";
import tableCategoryValueService from "./tableCategoryValueService";
import { PasswordUtils } from "../utils/passwordUtils";
export interface FormDataResult {
id: number;
@@ -860,33 +859,6 @@ export class DynamicFormService {
}
}
// 비밀번호(password) 타입 컬럼 처리
// - 빈 값이면 변경 목록에서 제거 (기존 비밀번호 유지)
// - 값이 있으면 암호화 후 저장
try {
const passwordCols = await query<{ column_name: string }>(
`SELECT DISTINCT column_name FROM table_type_columns
WHERE table_name = $1 AND input_type = 'password'`,
[tableName]
);
for (const { column_name } of passwordCols) {
if (column_name in changedFields) {
const pwValue = changedFields[column_name];
if (!pwValue || pwValue === "") {
// 빈 값 → 기존 비밀번호 유지 (변경 목록에서 제거)
delete changedFields[column_name];
console.log(`🔐 비밀번호 필드 ${column_name}: 빈 값이므로 업데이트 스킵 (기존 유지)`);
} else {
// 값 있음 → 암호화하여 저장
changedFields[column_name] = PasswordUtils.encrypt(pwValue);
console.log(`🔐 비밀번호 필드 ${column_name}: 새 비밀번호 암호화 완료`);
}
}
}
} catch (pwError) {
console.warn("⚠️ 비밀번호 컬럼 처리 중 오류:", pwError);
}
// 변경된 필드가 없으면 업데이트 건너뛰기
if (Object.keys(changedFields).length === 0) {
console.log("📋 변경된 필드가 없습니다. 업데이트를 건너뜁니다.");

View File

@@ -5177,8 +5177,18 @@ export class ScreenManagementService {
throw new Error("이 화면의 레이아웃을 저장할 권한이 없습니다.");
}
// 화면의 기본 테이블 업데이트 (테이블이 선택된 경우)
const mainTableName = layoutData.mainTableName;
if (mainTableName) {
await query(
`UPDATE screen_definitions SET table_name = $1, updated_date = NOW() WHERE screen_id = $2`,
[mainTableName, screenId],
);
console.log(`✅ [saveLayoutV2] 화면 기본 테이블 업데이트: ${mainTableName}`);
}
// 저장할 layout_data에서 레이어 메타 정보 제거 (순수 레이아웃만 저장)
const { layerId: _lid, layerName: _ln, conditionConfig: _cc, ...pureLayoutData } = layoutData;
const { layerId: _lid, layerName: _ln, conditionConfig: _cc, mainTableName: _mtn, ...pureLayoutData } = layoutData;
const dataToSave = {
version: "2.0",
...pureLayoutData,