Merge branch 'main' into feature/screen-management

This commit is contained in:
kjs
2025-12-01 15:22:07 +09:00
13 changed files with 494 additions and 202 deletions

View File

@@ -178,21 +178,24 @@ export class DashboardService {
let params: any[] = [];
let paramIndex = 1;
// 회사 코드 필터링 (최우선)
// 회사 코드 필터링 - company_code가 일치하면 해당 회사 사용자는 모두 조회 가능
if (companyCode) {
whereConditions.push(`d.company_code = $${paramIndex}`);
params.push(companyCode);
paramIndex++;
}
// 권한 필터링
if (userId) {
if (companyCode === '*') {
// 최고 관리자는 모든 대시보드 조회 가능
} else {
whereConditions.push(`d.company_code = $${paramIndex}`);
params.push(companyCode);
paramIndex++;
}
} else if (userId) {
// 회사 코드 없이 userId만 있는 경우 (본인 생성 또는 공개)
whereConditions.push(
`(d.created_by = $${paramIndex} OR d.is_public = true)`
);
params.push(userId);
paramIndex++;
} else {
// 비로그인 사용자는 공개 대시보드만
whereConditions.push("d.is_public = true");
}
@@ -228,7 +231,7 @@ export class DashboardService {
const whereClause = whereConditions.join(" AND ");
// 대시보드 목록 조회 (users 테이블 조인 제거)
// 대시보드 목록 조회 (user_info 조인하여 생성자 이름 포함)
const dashboardQuery = `
SELECT
d.id,
@@ -242,13 +245,16 @@ export class DashboardService {
d.tags,
d.category,
d.view_count,
d.company_code,
u.user_name as created_by_name,
COUNT(de.id) as elements_count
FROM dashboards d
LEFT JOIN dashboard_elements de ON d.id = de.dashboard_id
LEFT JOIN user_info u ON d.created_by = u.user_id
WHERE ${whereClause}
GROUP BY d.id, d.title, d.description, d.thumbnail_url, d.is_public,
d.created_by, d.created_at, d.updated_at, d.tags, d.category,
d.view_count
d.view_count, d.company_code, u.user_name
ORDER BY d.updated_at DESC
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
`;
@@ -277,12 +283,14 @@ export class DashboardService {
thumbnailUrl: row.thumbnail_url,
isPublic: row.is_public,
createdBy: row.created_by,
createdByName: row.created_by_name || row.created_by,
createdAt: row.created_at,
updatedAt: row.updated_at,
tags: JSON.parse(row.tags || "[]"),
category: row.category,
viewCount: parseInt(row.view_count || "0"),
elementsCount: parseInt(row.elements_count || "0"),
companyCode: row.company_code,
})),
pagination: {
page,

View File

@@ -124,6 +124,14 @@ export class BatchSchedulerService {
try {
logger.info(`배치 실행 시작: ${config.batch_name} (ID: ${config.id})`);
// 매핑 정보가 없으면 상세 조회로 다시 가져오기
if (!config.batch_mappings || config.batch_mappings.length === 0) {
const fullConfig = await BatchService.getBatchConfigById(config.id);
if (fullConfig.success && fullConfig.data) {
config = fullConfig.data;
}
}
// 실행 로그 생성
const executionLogResponse =
await BatchExecutionLogService.createExecutionLog({

View File

@@ -474,6 +474,105 @@ export class ExternalRestApiConnectionService {
}
}
/**
* 인증 헤더 생성
*/
static async getAuthHeaders(
authType: AuthType,
authConfig: any,
companyCode?: string
): Promise<Record<string, string>> {
const headers: Record<string, string> = {};
if (authType === "db-token") {
const cfg = authConfig || {};
const {
dbTableName,
dbValueColumn,
dbWhereColumn,
dbWhereValue,
dbHeaderName,
dbHeaderTemplate,
} = cfg;
if (!dbTableName || !dbValueColumn) {
throw new Error("DB 토큰 설정이 올바르지 않습니다.");
}
if (!companyCode) {
throw new Error("DB 토큰 모드에서는 회사 코드가 필요합니다.");
}
const hasWhereColumn = !!dbWhereColumn;
const hasWhereValue =
dbWhereValue !== undefined &&
dbWhereValue !== null &&
dbWhereValue !== "";
// where 컬럼/값은 둘 다 비우거나 둘 다 채워야 함
if (hasWhereColumn !== hasWhereValue) {
throw new Error(
"DB 토큰 설정에서 조건 컬럼과 조건 값은 둘 다 비우거나 둘 다 입력해야 합니다."
);
}
// 식별자 검증 (간단한 화이트리스트)
const identifierRegex = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
if (
!identifierRegex.test(dbTableName) ||
!identifierRegex.test(dbValueColumn) ||
(hasWhereColumn && !identifierRegex.test(dbWhereColumn as string))
) {
throw new Error(
"DB 토큰 설정에 유효하지 않은 테이블 또는 컬럼명이 포함되어 있습니다."
);
}
let sql = `
SELECT ${dbValueColumn} AS token_value
FROM ${dbTableName}
WHERE company_code = $1
`;
const params: any[] = [companyCode];
if (hasWhereColumn && hasWhereValue) {
sql += ` AND ${dbWhereColumn} = $2`;
params.push(dbWhereValue);
}
sql += `
ORDER BY updated_date DESC
LIMIT 1
`;
const tokenResult: QueryResult<any> = await pool.query(sql, params);
if (tokenResult.rowCount === 0) {
throw new Error("DB에서 토큰을 찾을 수 없습니다.");
}
const tokenValue = tokenResult.rows[0]["token_value"];
const headerName = dbHeaderName || "Authorization";
const template = dbHeaderTemplate || "Bearer {{value}}";
headers[headerName] = template.replace("{{value}}", tokenValue);
} else if (authType === "bearer" && authConfig?.token) {
headers["Authorization"] = `Bearer ${authConfig.token}`;
} else if (authType === "basic" && authConfig) {
const credentials = Buffer.from(
`${authConfig.username}:${authConfig.password}`
).toString("base64");
headers["Authorization"] = `Basic ${credentials}`;
} else if (authType === "api-key" && authConfig) {
if (authConfig.keyLocation === "header") {
headers[authConfig.keyName] = authConfig.keyValue;
}
}
return headers;
}
/**
* REST API 연결 테스트 (테스트 요청 데이터 기반)
*/
@@ -485,99 +584,15 @@ export class ExternalRestApiConnectionService {
try {
// 헤더 구성
const headers = { ...testRequest.headers };
let headers = { ...testRequest.headers };
// 인증 헤더 추가
if (testRequest.auth_type === "db-token") {
const cfg = testRequest.auth_config || {};
const {
dbTableName,
dbValueColumn,
dbWhereColumn,
dbWhereValue,
dbHeaderName,
dbHeaderTemplate,
} = cfg;
if (!dbTableName || !dbValueColumn) {
throw new Error("DB 토큰 설정이 올바르지 않습니다.");
}
if (!userCompanyCode) {
throw new Error("DB 토큰 모드에서는 회사 코드가 필요합니다.");
}
const hasWhereColumn = !!dbWhereColumn;
const hasWhereValue =
dbWhereValue !== undefined && dbWhereValue !== null && dbWhereValue !== "";
// where 컬럼/값은 둘 다 비우거나 둘 다 채워야 함
if (hasWhereColumn !== hasWhereValue) {
throw new Error(
"DB 토큰 설정에서 조건 컬럼과 조건 값은 둘 다 비우거나 둘 다 입력해야 합니다."
);
}
// 식별자 검증 (간단한 화이트리스트)
const identifierRegex = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
if (
!identifierRegex.test(dbTableName) ||
!identifierRegex.test(dbValueColumn) ||
(hasWhereColumn && !identifierRegex.test(dbWhereColumn as string))
) {
throw new Error(
"DB 토큰 설정에 유효하지 않은 테이블 또는 컬럼명이 포함되어 있습니다."
);
}
let sql = `
SELECT ${dbValueColumn} AS token_value
FROM ${dbTableName}
WHERE company_code = $1
`;
const params: any[] = [userCompanyCode];
if (hasWhereColumn && hasWhereValue) {
sql += ` AND ${dbWhereColumn} = $2`;
params.push(dbWhereValue);
}
sql += `
ORDER BY updated_date DESC
LIMIT 1
`;
const tokenResult: QueryResult<any> = await pool.query(sql, params);
if (tokenResult.rowCount === 0) {
throw new Error("DB에서 토큰을 찾을 수 없습니다.");
}
const tokenValue = tokenResult.rows[0]["token_value"];
const headerName = dbHeaderName || "Authorization";
const template = dbHeaderTemplate || "Bearer {{value}}";
headers[headerName] = template.replace("{{value}}", tokenValue);
} else if (
testRequest.auth_type === "bearer" &&
testRequest.auth_config?.token
) {
headers["Authorization"] = `Bearer ${testRequest.auth_config.token}`;
} else if (testRequest.auth_type === "basic" && testRequest.auth_config) {
const credentials = Buffer.from(
`${testRequest.auth_config.username}:${testRequest.auth_config.password}`
).toString("base64");
headers["Authorization"] = `Basic ${credentials}`;
} else if (
testRequest.auth_type === "api-key" &&
testRequest.auth_config
) {
if (testRequest.auth_config.keyLocation === "header") {
headers[testRequest.auth_config.keyName] =
testRequest.auth_config.keyValue;
}
}
// 인증 헤더 생성 및 병합
const authHeaders = await this.getAuthHeaders(
testRequest.auth_type,
testRequest.auth_config,
userCompanyCode
);
headers = { ...headers, ...authHeaders };
// URL 구성
let url = testRequest.base_url;